diff --git a/packages/api-fetch/src/middlewares/preloading.js b/packages/api-fetch/src/middlewares/preloading.js
index 020cb913fced15..4de356243b7366 100644
--- a/packages/api-fetch/src/middlewares/preloading.js
+++ b/packages/api-fetch/src/middlewares/preloading.js
@@ -34,7 +34,11 @@ const createPreloadingMiddleware = ( preloadedData ) => ( options, next ) => {
 
 		if ( parse && 'GET' === method && preloadedData[ path ] ) {
 			return Promise.resolve( preloadedData[ path ].body );
-		} else if ( 'OPTIONS' === method && preloadedData[ method ][ path ] ) {
+		} else if (
+			'OPTIONS' === method &&
+			preloadedData[ method ] &&
+			preloadedData[ method ][ path ]
+		) {
 			return Promise.resolve( preloadedData[ method ][ path ] );
 		}
 	}
diff --git a/packages/api-fetch/src/middlewares/test/preloading.js b/packages/api-fetch/src/middlewares/test/preloading.js
index d697af3b429494..e2d8a1f4d0c68d 100644
--- a/packages/api-fetch/src/middlewares/test/preloading.js
+++ b/packages/api-fetch/src/middlewares/test/preloading.js
@@ -25,20 +25,29 @@ describe( 'Preloading Middleware', () => {
 		} );
 	} );
 
-	it( 'should move to the next middleware if no preloaded data', () => {
-		const preloadedData = {};
-		const prelooadingMiddleware = createPreloadingMiddleware( preloadedData );
-		const requestOptions = {
-			method: 'GET',
-			path: 'wp/v2/posts',
-		};
+	describe.each( [
+		[ 'GET' ],
+		[ 'OPTIONS' ],
+	] )( '%s', ( method ) => {
+		describe.each( [
+			[ 'all empty', {} ],
+			[ 'method empty', { [ method ]: {} } ],
+		] )( '%s', ( label, preloadedData ) => {
+			it( 'should move to the next middleware if no preloaded data', () => {
+				const prelooadingMiddleware = createPreloadingMiddleware( preloadedData );
+				const requestOptions = {
+					method,
+					path: 'wp/v2/posts',
+				};
 
-		const callback = ( options ) => {
-			expect( options ).toBe( requestOptions );
-			return true;
-		};
+				const callback = ( options ) => {
+					expect( options ).toBe( requestOptions );
+					return true;
+				};
 
-		const ret = prelooadingMiddleware( requestOptions, callback );
-		expect( ret ).toBe( true );
+				const ret = prelooadingMiddleware( requestOptions, callback );
+				expect( ret ).toBe( true );
+			} );
+		} );
 	} );
 } );
diff --git a/packages/block-editor/CHANGELOG.md b/packages/block-editor/CHANGELOG.md
index 5c4cae61450ac4..4306b855955f52 100644
--- a/packages/block-editor/CHANGELOG.md
+++ b/packages/block-editor/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 2.0.0 (Unreleased)
+
+### Breaking Changes
+
+- `CopyHandler` will now only catch cut/copy events coming from its `props.children`, instead of from anywhere in the `document`.
+
 ## 1.0.0 (2019-03-06)
 
 ### New Features
diff --git a/packages/block-editor/src/components/block-list/style.scss b/packages/block-editor/src/components/block-list/style.scss
index 4a72cb3ed3423d..5f48fe5a165dba 100644
--- a/packages/block-editor/src/components/block-list/style.scss
+++ b/packages/block-editor/src/components/block-list/style.scss
@@ -904,6 +904,12 @@
 	.block-editor-block-contextual-toolbar > * {
 		pointer-events: auto;
 	}
+
+	// Full-aligned blocks have negative margins on the parent of the toolbar, so additional position adjustment is not required.
+	&[data-align="full"] .block-editor-block-contextual-toolbar {
+		left: 0;
+		right: 0;
+	}
 }
 
 .block-editor-block-list__block.is-focus-mode:not(.is-multi-selected) > .block-editor-block-contextual-toolbar {
diff --git a/packages/block-editor/src/components/block-settings-menu/block-convert-button.js b/packages/block-editor/src/components/block-settings-menu/block-convert-button.js
index 2ae65fed66cfd9..f27cac38b1df9a 100644
--- a/packages/block-editor/src/components/block-settings-menu/block-convert-button.js
+++ b/packages/block-editor/src/components/block-settings-menu/block-convert-button.js
@@ -15,7 +15,6 @@ export default function BlockConvertButton( { shouldRender, onClick, small } ) {
 			className="editor-block-settings-menu__control block-editor-block-settings-menu__control"
 			onClick={ onClick }
 			icon="screenoptions"
-			label={ small ? label : undefined }
 		>
 			{ ! small && label }
 		</MenuItem>
diff --git a/packages/block-editor/src/components/block-settings-menu/block-mode-toggle.js b/packages/block-editor/src/components/block-settings-menu/block-mode-toggle.js
index 8ba029fd1794c1..3f8260d135e441 100644
--- a/packages/block-editor/src/components/block-settings-menu/block-mode-toggle.js
+++ b/packages/block-editor/src/components/block-settings-menu/block-mode-toggle.js
@@ -26,7 +26,6 @@ export function BlockModeToggle( { blockType, mode, onToggleMode, small = false
 			className="editor-block-settings-menu__control block-editor-block-settings-menu__control"
 			onClick={ onToggleMode }
 			icon="html"
-			label={ small ? label : undefined }
 		>
 			{ ! small && label }
 		</MenuItem>
diff --git a/packages/block-editor/src/components/copy-handler/index.js b/packages/block-editor/src/components/copy-handler/index.js
index 3942eaae71b034..3f73d5f672c153 100644
--- a/packages/block-editor/src/components/copy-handler/index.js
+++ b/packages/block-editor/src/components/copy-handler/index.js
@@ -1,33 +1,17 @@
 /**
  * WordPress dependencies
  */
-import { Component } from '@wordpress/element';
 import { serialize } from '@wordpress/blocks';
 import { documentHasSelection } from '@wordpress/dom';
 import { withDispatch } from '@wordpress/data';
 import { compose } from '@wordpress/compose';
 
-class CopyHandler extends Component {
-	constructor() {
-		super( ...arguments );
-
-		this.onCopy = ( event ) => this.props.onCopy( event );
-		this.onCut = ( event ) => this.props.onCut( event );
-	}
-
-	componentDidMount() {
-		document.addEventListener( 'copy', this.onCopy );
-		document.addEventListener( 'cut', this.onCut );
-	}
-
-	componentWillUnmount() {
-		document.removeEventListener( 'copy', this.onCopy );
-		document.removeEventListener( 'cut', this.onCut );
-	}
-
-	render() {
-		return null;
-	}
+function CopyHandler( { children, onCopy, onCut } ) {
+	return (
+		<div onCopy={ onCopy } onCut={ onCut }>
+			{ children }
+		</div>
+	);
 }
 
 export default compose( [
diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js
index 006ec4a697760b..c6cfad310c89c6 100644
--- a/packages/block-editor/src/components/rich-text/index.js
+++ b/packages/block-editor/src/components/rich-text/index.js
@@ -42,6 +42,8 @@ import {
 	isCollapsed,
 	LINE_SEPARATOR,
 	indentListItems,
+	__unstableGetActiveFormats,
+	__unstableUpdateFormats,
 } from '@wordpress/rich-text';
 import { decodeEntities } from '@wordpress/html-entities';
 import { withFilters, IsolatedEventContainer } from '@wordpress/components';
@@ -82,6 +84,13 @@ const INSERTION_INPUT_TYPES_TO_IGNORE = new Set( [
 	'insertLink',
 ] );
 
+/**
+ * Global stylesheet.
+ */
+const globalStyle = document.createElement( 'style' );
+
+document.head.appendChild( globalStyle );
+
 export class RichText extends Component {
 	constructor( { value, onReplace, multiline } ) {
 		super( ...arguments );
@@ -126,7 +135,10 @@ export class RichText extends Component {
 		this.handleHorizontalNavigation = this.handleHorizontalNavigation.bind( this );
 		this.onPointerDown = this.onPointerDown.bind( this );
 
-		this.formatToValue = memize( this.formatToValue.bind( this ), { size: 1 } );
+		this.formatToValue = memize(
+			this.formatToValue.bind( this ),
+			{ maxSize: 1 }
+		);
 
 		this.savedContent = value;
 		this.patterns = getPatterns( {
@@ -176,9 +188,9 @@ export class RichText extends Component {
 	 */
 	getRecord() {
 		const { formats, replacements, text } = this.formatToValue( this.props.value );
-		const { start, end, selectedFormat } = this.state;
+		const { start, end, activeFormats } = this.state;
 
-		return { formats, replacements, text, start, end, selectedFormat };
+		return { formats, replacements, text, start, end, activeFormats };
 	}
 
 	createRecord() {
@@ -355,6 +367,8 @@ export class RichText extends Component {
 			unstableOnFocus();
 		}
 
+		this.recalculateBoundaryStyle();
+
 		document.addEventListener( 'selectionchange', this.onSelectionChange );
 	}
 
@@ -393,40 +407,19 @@ export class RichText extends Component {
 			}
 		}
 
-		let { selectedFormat } = this.state;
-		const { formats, replacements, text, start, end } = this.createRecord();
-
-		if ( this.formatPlaceholder ) {
-			selectedFormat = this.formatPlaceholder.length;
-
-			if ( selectedFormat > 0 ) {
-				formats[ this.state.start ] = this.formatPlaceholder;
-			} else {
-				delete formats[ this.state.start ];
-			}
-		} else if ( selectedFormat > 0 ) {
-			const formatsBefore = formats[ start - 1 ] || [];
-			const formatsAfter = formats[ start ] || [];
-
-			let source = formatsBefore;
-
-			if ( formatsAfter.length > formatsBefore.length ) {
-				source = formatsAfter;
-			}
-
-			source = source.slice( 0, selectedFormat );
-
-			formats[ this.state.start ] = source;
-		} else {
-			delete formats[ this.state.start ];
-		}
-
-		const change = { formats, replacements, text, start, end, selectedFormat };
+		const value = this.createRecord();
+		const { activeFormats = [], start } = this.state;
 
-		this.onChange( change, {
-			withoutHistory: true,
+		// Update the formats between the last and new caret position.
+		const change = __unstableUpdateFormats( {
+			value,
+			start,
+			end: value.start,
+			formats: activeFormats,
 		} );
 
+		this.onChange( change, { withoutHistory: true } );
+
 		const transformed = this.patterns.reduce(
 			( accumlator, transform ) => transform( accumlator ),
 			change
@@ -434,7 +427,7 @@ export class RichText extends Component {
 
 		if ( transformed !== change ) {
 			this.onCreateUndoLevel();
-			this.onChange( { ...transformed, selectedFormat } );
+			this.onChange( { ...transformed, activeFormats } );
 		}
 
 		// Create an undo level when input stops for over a second.
@@ -454,36 +447,40 @@ export class RichText extends Component {
 	 * Handles the `selectionchange` event: sync the selection to local state.
 	 */
 	onSelectionChange() {
-		if ( this.ignoreSelectionChange ) {
-			delete this.ignoreSelectionChange;
-			return;
-		}
-
 		const value = this.createRecord();
-		const { start, end, formats } = value;
+		const { start, end } = value;
 
 		if ( start !== this.state.start || end !== this.state.end ) {
-			const isCaretWithinFormattedText = this.props.isCaretWithinFormattedText;
+			const { isCaretWithinFormattedText } = this.props;
+			const activeFormats = __unstableGetActiveFormats( value );
 
-			if ( ! isCaretWithinFormattedText && formats[ start ] ) {
+			if ( ! isCaretWithinFormattedText && activeFormats.length ) {
 				this.props.onEnterFormattedText();
-			} else if ( isCaretWithinFormattedText && ! formats[ start ] ) {
+			} else if ( isCaretWithinFormattedText && ! activeFormats.length ) {
 				this.props.onExitFormattedText();
 			}
 
-			let selectedFormat;
-
-			if ( isCollapsed( value ) ) {
-				const formatsBefore = formats[ start - 1 ] || [];
-				const formatsAfter = formats[ start ] || [];
+			this.setState( { start, end, activeFormats } );
+			this.applyRecord( { ...value, activeFormats }, { domOnly: true } );
 
-				selectedFormat = Math.min( formatsBefore.length, formatsAfter.length );
+			if ( activeFormats.length > 0 ) {
+				this.recalculateBoundaryStyle();
 			}
+		}
+	}
+
+	recalculateBoundaryStyle() {
+		const boundarySelector = '*[data-rich-text-format-boundary]';
+		const element = this.editableRef.querySelector( boundarySelector );
 
-			this.setState( { start, end, selectedFormat } );
-			this.applyRecord( { ...value, selectedFormat }, { domOnly: true } );
+		if ( element ) {
+			const computedStyle = getComputedStyle( element );
+			const newColor = computedStyle.color
+				.replace( ')', ', 0.2)' )
+				.replace( 'rgb', 'rgba' );
 
-			delete this.formatPlaceholder;
+			globalStyle.innerHTML =
+				`*:focus ${ boundarySelector }{background-color: ${ newColor }}`;
 		}
 	}
 
@@ -511,14 +508,12 @@ export class RichText extends Component {
 	onChange( record, { withoutHistory } = {} ) {
 		this.applyRecord( record );
 
-		const { start, end, formatPlaceholder, selectedFormat } = record;
+		const { start, end, activeFormats = [] } = record;
 
-		this.formatPlaceholder = formatPlaceholder;
 		this.onChangeEditableValue( record );
-
 		this.savedContent = this.valueToFormat( record );
 		this.props.onChange( this.savedContent );
-		this.setState( { start, end, selectedFormat } );
+		this.setState( { start, end, activeFormats } );
 
 		if ( ! withoutHistory ) {
 			this.onCreateUndoLevel();
@@ -734,17 +729,15 @@ export class RichText extends Component {
 	handleHorizontalNavigation( event ) {
 		const value = this.createRecord();
 		const { formats, text, start, end } = value;
-		const { selectedFormat } = this.state;
+		const { activeFormats = [] } = this.state;
 		const collapsed = isCollapsed( value );
 		const isReverse = event.keyCode === LEFT;
 
-		delete this.formatPlaceholder;
-
 		// If the selection is collapsed and at the very start, do nothing if
 		// navigating backward.
 		// If the selection is collapsed and at the very end, do nothing if
 		// navigating forward.
-		if ( collapsed && selectedFormat === 0 ) {
+		if ( collapsed && activeFormats.length === 0 ) {
 			if ( start === 0 && isReverse ) {
 				return;
 			}
@@ -764,38 +757,43 @@ export class RichText extends Component {
 		// In all other cases, prevent default behaviour.
 		event.preventDefault();
 
-		// Ignore the selection change handler when setting selection, all state
-		// will be set here.
-		this.ignoreSelectionChange = true;
-
 		const formatsBefore = formats[ start - 1 ] || [];
 		const formatsAfter = formats[ start ] || [];
 
-		let newSelectedFormat = selectedFormat;
+		let newActiveFormatsLength = activeFormats.length;
+		let source = formatsAfter;
+
+		if ( formatsBefore.length > formatsAfter.length ) {
+			source = formatsBefore;
+		}
 
 		// If the amount of formats before the caret and after the caret is
 		// different, the caret is at a format boundary.
 		if ( formatsBefore.length < formatsAfter.length ) {
-			if ( ! isReverse && selectedFormat < formatsAfter.length ) {
-				newSelectedFormat++;
+			if ( ! isReverse && activeFormats.length < formatsAfter.length ) {
+				newActiveFormatsLength++;
 			}
 
-			if ( isReverse && selectedFormat > formatsBefore.length ) {
-				newSelectedFormat--;
+			if ( isReverse && activeFormats.length > formatsBefore.length ) {
+				newActiveFormatsLength--;
 			}
 		} else if ( formatsBefore.length > formatsAfter.length ) {
-			if ( ! isReverse && selectedFormat > formatsAfter.length ) {
-				newSelectedFormat--;
+			if ( ! isReverse && activeFormats.length > formatsAfter.length ) {
+				newActiveFormatsLength--;
 			}
 
-			if ( isReverse && selectedFormat < formatsBefore.length ) {
-				newSelectedFormat++;
+			if ( isReverse && activeFormats.length < formatsBefore.length ) {
+				newActiveFormatsLength++;
 			}
 		}
 
-		if ( newSelectedFormat !== selectedFormat ) {
-			this.applyRecord( { ...value, selectedFormat: newSelectedFormat } );
-			this.setState( { selectedFormat: newSelectedFormat } );
+		// Wait for boundary class to be added.
+		setTimeout( () => this.recalculateBoundaryStyle() );
+
+		if ( newActiveFormatsLength !== activeFormats.length ) {
+			const newActiveFormats = source.slice( 0, newActiveFormatsLength );
+			this.applyRecord( { ...value, activeFormats: newActiveFormats } );
+			this.setState( { activeFormats: newActiveFormats } );
 			return;
 		}
 
@@ -806,7 +804,7 @@ export class RichText extends Component {
 			...value,
 			start: newPos,
 			end: newPos,
-			selectedFormat: isReverse ? formatsBefore.length : formatsAfter.length,
+			activeFormats: isReverse ? formatsBefore : formatsAfter,
 		} );
 	}
 
diff --git a/packages/block-editor/src/components/rich-text/style.scss b/packages/block-editor/src/components/rich-text/style.scss
index 16fdc282020e0c..4a52ddb56fcd37 100644
--- a/packages/block-editor/src/components/rich-text/style.scss
+++ b/packages/block-editor/src/components/rich-text/style.scss
@@ -45,27 +45,6 @@
 
 		*[data-rich-text-format-boundary] {
 			border-radius: 2px;
-			box-shadow: 0 0 0 1px $light-gray-400;
-			background: $light-gray-400;
-
-			// Enforce a dark text color so active inline boundaries
-			// are always readable.
-			// See https://github.com/WordPress/gutenberg/issues/9508
-			color: $dark-gray-900;
-		}
-
-		// Link inline boundaries get special colors.
-		a[data-rich-text-format-boundary] {
-			box-shadow: 0 0 0 1px $blue-medium-100;
-			background: $blue-medium-100;
-			color: $blue-medium-900;
-		}
-
-		// <code> inline boundaries need special treatment because their
-		// un-selected style is already padded.
-		code[data-rich-text-format-boundary] {
-			background: $light-gray-400;
-			box-shadow: 0 0 0 1px $light-gray-400;
 		}
 	}
 
diff --git a/packages/block-library/src/cover/editor.scss b/packages/block-library/src/cover/editor.scss
index c74979dc0969d4..b368b3ee24c05d 100644
--- a/packages/block-library/src/cover/editor.scss
+++ b/packages/block-library/src/cover/editor.scss
@@ -1,10 +1,5 @@
 .wp-block-cover-image,
 .wp-block-cover {
-	.block-editor-rich-text__editable:focus a[data-rich-text-format-boundary] {
-		box-shadow: none;
-		background: rgba(255, 255, 255, 0.3);
-	}
-
 	&.components-placeholder h2 {
 		color: inherit;
 	}
diff --git a/packages/block-library/src/embed/edit.js b/packages/block-library/src/embed/edit.js
index 0e251a8f0aafd0..fcc305d9e8a7c3 100644
--- a/packages/block-library/src/embed/edit.js
+++ b/packages/block-library/src/embed/edit.js
@@ -61,13 +61,24 @@ export function getEmbedEditComponent( title, icon, responsive = true ) {
 
 			if ( switchedPreview || switchedURL ) {
 				if ( this.props.cannotEmbed ) {
-					// Can't embed this URL, and we've just received or switched the preview.
+					// We either have a new preview or a new URL, but we can't embed it.
+					if ( ! this.props.fetching ) {
+						// If we're not fetching the preview, then we know it can't be embedded, so try
+						// removing any trailing slash, and resubmit.
+						this.resubmitWithoutTrailingSlash();
+					}
 					return;
 				}
 				this.handleIncomingPreview();
 			}
 		}
 
+		resubmitWithoutTrailingSlash() {
+			this.setState( ( prevState ) => ( {
+				url: prevState.url.replace( /\/$/, '' ),
+			} ), this.setUrl );
+		}
+
 		setUrl( event ) {
 			if ( event ) {
 				event.preventDefault();
diff --git a/packages/block-library/src/embed/util.js b/packages/block-library/src/embed/util.js
index 085ea4b22ff8c8..9cba9f50364745 100644
--- a/packages/block-library/src/embed/util.js
+++ b/packages/block-library/src/embed/util.js
@@ -46,7 +46,7 @@ export const findBlock = ( url ) => {
 };
 
 export const isFromWordPress = ( html ) => {
-	return includes( html, 'class="wp-embedded-content" data-secret' );
+	return includes( html, 'class="wp-embedded-content"' );
 };
 
 export const getPhotoHtml = ( photo ) => {
diff --git a/packages/block-library/src/gallery/editor.scss b/packages/block-library/src/gallery/editor.scss
index 0340f66a31e8e5..34b08797e2e558 100644
--- a/packages/block-library/src/gallery/editor.scss
+++ b/packages/block-library/src/gallery/editor.scss
@@ -82,10 +82,6 @@ ul.wp-block-gallery li {
 		a {
 			color: $white;
 		}
-
-		&:focus a[data-rich-text-format-boundary] {
-			color: rgba(0, 0, 0, 0.2);
-		}
 	}
 }
 
diff --git a/packages/block-library/src/preformatted/index.js b/packages/block-library/src/preformatted/index.js
index 7d90224f9639cd..9553ac77885f98 100644
--- a/packages/block-library/src/preformatted/index.js
+++ b/packages/block-library/src/preformatted/index.js
@@ -68,10 +68,14 @@ export const settings = {
 		return (
 			<RichText
 				tagName="pre"
+				// Ensure line breaks are normalised to HTML.
 				value={ content.replace( /\n/g, '<br>' ) }
 				onChange={ ( nextContent ) => {
 					setAttributes( {
-						content: nextContent,
+						// Ensure line breaks are normalised to characters. This
+						// saves space, is easier to read, and ensures display
+						// filters work correctly.
+						content: nextContent.replace( /<br ?\/?>/g, '\n' ),
 					} );
 				} }
 				placeholder={ __( 'Write preformatted text…' ) }
diff --git a/packages/blocks/src/api/raw-handling/paste-handler.js b/packages/blocks/src/api/raw-handling/paste-handler.js
index b40164f549689b..1d67540c0b9225 100644
--- a/packages/blocks/src/api/raw-handling/paste-handler.js
+++ b/packages/blocks/src/api/raw-handling/paste-handler.js
@@ -127,9 +127,14 @@ export function pasteHandler( { HTML = '', plainText = '', mode = 'AUTO', tagNam
 	// First of all, strip any meta tags.
 	HTML = HTML.replace( /<meta[^>]+>/, '' );
 
-	// If we detect block delimiters, parse entirely as blocks.
-	if ( mode !== 'INLINE' && HTML.indexOf( '<!-- wp:' ) !== -1 ) {
-		return parseWithGrammar( HTML );
+	// If we detect block delimiters in HTML, parse entirely as blocks.
+	if ( mode !== 'INLINE' ) {
+		// Check plain text if there is no HTML.
+		const content = HTML ? HTML : plainText;
+
+		if ( content.indexOf( '<!-- wp:' ) !== -1 ) {
+			return parseWithGrammar( content );
+		}
 	}
 
 	// Normalize unicode to use composed characters.
diff --git a/packages/components/src/menu-item/README.md b/packages/components/src/menu-item/README.md
index eacd6770f7aaf3..38251ccedf2614 100644
--- a/packages/components/src/menu-item/README.md
+++ b/packages/components/src/menu-item/README.md
@@ -34,15 +34,6 @@ Element to render as child of button.
 
 Element
 
-### `label`
-
-- Type: `string`
-- Required: No
-
-String to use as primary button label text, applied as `aria-label`. Useful in cases where an `info` prop is passed, where `label` should be the minimal text of the button, described in further detail by `info`.
-
-Defaults to the value of `children`, if `children` is passed as a string.
-
 ### `info`
 
 - Type: `string`
diff --git a/packages/components/src/menu-item/index.js b/packages/components/src/menu-item/index.js
index 860430b4ecb67c..1b793a2c5cf265 100644
--- a/packages/components/src/menu-item/index.js
+++ b/packages/components/src/menu-item/index.js
@@ -30,7 +30,6 @@ export function MenuItem( {
 	shortcut,
 	isSelected,
 	role = 'menuitem',
-	instanceId,
 	...props
 } ) {
 	className = classnames( 'components-menu-item__button', className, {
@@ -38,16 +37,10 @@ export function MenuItem( {
 	} );
 
 	if ( info ) {
-		const infoId = 'edit-post-feature-toggle__info-' + instanceId;
-
-		// Deconstructed props is scoped to the function; mutation is fine.
-		props[ 'aria-describedby' ] = infoId;
-
 		children = (
 			<span className="components-menu-item__info-wrapper">
 				{ children }
 				<span
-					id={ infoId }
 					className="components-menu-item__info">
 					{ info }
 				</span>
diff --git a/packages/components/src/menu-item/test/__snapshots__/index.js.snap b/packages/components/src/menu-item/test/__snapshots__/index.js.snap
index a0f00b2889f008..d029c29c0cb3aa 100644
--- a/packages/components/src/menu-item/test/__snapshots__/index.js.snap
+++ b/packages/components/src/menu-item/test/__snapshots__/index.js.snap
@@ -18,7 +18,6 @@ exports[`MenuItem should match snapshot when all props provided 1`] = `
 
 exports[`MenuItem should match snapshot when info is provided 1`] = `
 <ForwardRef(Button)
-  aria-describedby="edit-post-feature-toggle__info-1"
   className="components-menu-item__button"
   role="menuitem"
 >
@@ -28,7 +27,6 @@ exports[`MenuItem should match snapshot when info is provided 1`] = `
     My item
     <span
       className="components-menu-item__info"
-      id="edit-post-feature-toggle__info-1"
     >
       Extended description of My Item
     </span>
diff --git a/packages/components/src/menu-item/test/index.js b/packages/components/src/menu-item/test/index.js
index f521add7452c86..63ac41b337dff9 100644
--- a/packages/components/src/menu-item/test/index.js
+++ b/packages/components/src/menu-item/test/index.js
@@ -54,7 +54,7 @@ describe( 'MenuItem', () => {
 
 	it( 'should match snapshot when info is provided', () => {
 		const wrapper = shallow(
-			<MenuItem info="Extended description of My Item" instanceId={ 1 }>
+			<MenuItem info="Extended description of My Item">
 				My item
 			</MenuItem>
 		);
diff --git a/packages/dom/README.md b/packages/dom/README.md
index bf572a4741b508..c6b99455456023 100644
--- a/packages/dom/README.md
+++ b/packages/dom/README.md
@@ -170,7 +170,7 @@ Check whether the selection is vertically at the edge of the container.
 
 **Returns**
 
-`boolean`: True if at the edge, false if not.
+`boolean`: True if at the vertical edge, false if not.
 
 ### placeCaretAtHorizontalEdge
 
diff --git a/packages/dom/src/dom.js b/packages/dom/src/dom.js
index 4ea9ad8c0ef78f..87be698a3be8d0 100644
--- a/packages/dom/src/dom.js
+++ b/packages/dom/src/dom.js
@@ -60,14 +60,17 @@ function isSelectionForward( selection ) {
 }
 
 /**
- * Check whether the selection is horizontally at the edge of the container.
+ * Check whether the selection is at the edge of the container. Checks for
+ * horizontal position by default. Set `onlyVertical` to true to check only
+ * vertically.
  *
- * @param {Element} container Focusable element.
- * @param {boolean} isReverse Set to true to check left, false for right.
+ * @param {Element} container    Focusable element.
+ * @param {boolean} isReverse    Set to true to check left, false to check right.
+ * @param {boolean} onlyVertical Set to true to check only vertical position.
  *
- * @return {boolean} True if at the horizontal edge, false if not.
+ * @return {boolean} True if at the edge, false if not.
  */
-export function isHorizontalEdge( container, isReverse ) {
+function isEdge( container, isReverse, onlyVertical ) {
 	if ( includes( [ 'INPUT', 'TEXTAREA' ], container.tagName ) ) {
 		if ( container.selectionStart !== container.selectionEnd ) {
 			return false;
@@ -86,131 +89,88 @@ export function isHorizontalEdge( container, isReverse ) {
 
 	const selection = window.getSelection();
 
-	// Create copy of range for setting selection to find effective offset.
-	const range = selection.getRangeAt( 0 ).cloneRange();
-
-	// Collapse in direction of selection.
-	if ( ! selection.isCollapsed ) {
-		range.collapse( ! isSelectionForward( selection ) );
-	}
-
-	let node = range.startContainer;
-
-	let extentOffset;
-	if ( isReverse ) {
-		// When in reverse, range node should be first.
-		extentOffset = 0;
-	} else if ( node.nodeValue ) {
-		// Otherwise, vary by node type. A text node has no children. Its range
-		// offset reflects its position in nodeValue.
-		//
-		// "If the startContainer is a Node of type Text, Comment, or
-		// CDATASection, then the offset is the number of characters from the
-		// start of the startContainer to the boundary point of the Range."
-		//
-		// See: https://developer.mozilla.org/en-US/docs/Web/API/Range/startOffset
-		// See: https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeValue
-		extentOffset = node.nodeValue.length;
-	} else {
-		// "For other Node types, the startOffset is the number of child nodes
-		// between the start of the startContainer and the boundary point of
-		// the Range."
-		//
-		// See: https://developer.mozilla.org/en-US/docs/Web/API/Range/startOffset
-		extentOffset = node.childNodes.length;
-	}
-
-	// Offset of range should be at expected extent.
-	const position = isReverse ? 'start' : 'end';
-	const offset = range[ `${ position }Offset` ];
-	if ( offset !== extentOffset ) {
+	if ( ! selection.rangeCount ) {
 		return false;
 	}
 
-	// If confirmed to be at extent, traverse up through DOM, verifying that
-	// the node is at first or last child for reverse or forward respectively
-	// (ignoring empty text nodes). Continue until container is reached.
-	const order = isReverse ? 'previous' : 'next';
-
-	while ( node !== container ) {
-		let next = node[ `${ order }Sibling` ];
-
-		// Skip over empty text nodes.
-		while ( next && next.nodeType === TEXT_NODE && next.data === '' ) {
-			next = next[ `${ order }Sibling` ];
-		}
-
-		if ( next ) {
-			return false;
-		}
-
-		node = node.parentNode;
-	}
-
-	// If reached, range is assumed to be at edge.
-	return true;
-}
-
-/**
- * Check whether the selection is vertically at the edge of the container.
- *
- * @param {Element} container Focusable element.
- * @param {boolean} isReverse Set to true to check top, false for bottom.
- *
- * @return {boolean} True if at the edge, false if not.
- */
-export function isVerticalEdge( container, isReverse ) {
-	if ( includes( [ 'INPUT', 'TEXTAREA' ], container.tagName ) ) {
-		return isHorizontalEdge( container, isReverse );
-	}
+	const rangeRect = getRectangleFromRange( selection.getRangeAt( 0 ) );
 
-	if ( ! container.isContentEditable ) {
-		return true;
+	if ( ! rangeRect ) {
+		return false;
 	}
 
-	const selection = window.getSelection();
+	const computedStyle = window.getComputedStyle( container );
+	const lineHeight = parseInt( computedStyle.lineHeight, 10 );
 
-	// Only consider the selection at the edge if the direction is towards the
-	// edge.
+	// Only consider the multiline selection at the edge if the direction is
+	// towards the edge.
 	if (
 		! selection.isCollapsed &&
+		rangeRect.height > lineHeight &&
 		isSelectionForward( selection ) === isReverse
 	) {
 		return false;
 	}
 
-	const range = selection.rangeCount ? selection.getRangeAt( 0 ) : null;
+	// Calculate a buffer that is half the line height. In some browsers, the
+	// selection rectangle may not fill the entire height of the line, so we add
+	// 3/4 the line height to the selection rectangle to ensure that it is well
+	// over its line boundary.
+	const buffer = 3 * parseInt( lineHeight, 10 ) / 4;
+	const containerRect = container.getBoundingClientRect();
+	const verticalEdge = isReverse ?
+		containerRect.top > rangeRect.top - buffer :
+		containerRect.bottom < rangeRect.bottom + buffer;
 
-	if ( ! range ) {
+	if ( ! verticalEdge ) {
 		return false;
 	}
 
-	const rangeRect = getRectangleFromRange( range );
-
-	if ( ! rangeRect ) {
-		return false;
+	if ( onlyVertical ) {
+		return true;
 	}
 
-	const editableRect = container.getBoundingClientRect();
-
-	// Calculate a buffer that is half the line height. In some browsers, the
-	// selection rectangle may not fill the entire height of the line, so we add
-	// half the line height to the selection rectangle to ensure that it is well
-	// over its line boundary.
-	const { lineHeight } = window.getComputedStyle( container );
-	const buffer = parseInt( lineHeight, 10 ) / 2;
+	// To calculate the horizontal position, we insert a test range and see if
+	// this test range has the same horizontal position. This method proves to
+	// be better than a DOM-based calculation, because it ignores empty text
+	// nodes and a trailing line break element. In other words, we need to check
+	// visual positioning, not DOM positioning.
+	const x = isReverse ? containerRect.left + 1 : containerRect.right - 1;
+	const y = isReverse ? containerRect.top + buffer : containerRect.bottom - buffer;
+	const testRange = hiddenCaretRangeFromPoint( document, x, y, container );
 
-	// Too low.
-	if ( isReverse && rangeRect.top - buffer > editableRect.top ) {
+	if ( ! testRange ) {
 		return false;
 	}
 
-	// Too high.
-	if ( ! isReverse && rangeRect.bottom + buffer < editableRect.bottom ) {
-		return false;
-	}
+	const side = isReverse ? 'left' : 'right';
+	const testRect = getRectangleFromRange( testRange );
 
-	return true;
+	return Math.round( testRect[ side ] ) === Math.round( rangeRect[ side ] );
+}
+
+/**
+ * Check whether the selection is horizontally at the edge of the container.
+ *
+ * @param {Element} container Focusable element.
+ * @param {boolean} isReverse Set to true to check left, false for right.
+ *
+ * @return {boolean} True if at the horizontal edge, false if not.
+ */
+export function isHorizontalEdge( container, isReverse ) {
+	return isEdge( container, isReverse );
+}
+
+/**
+ * Check whether the selection is vertically at the edge of the container.
+ *
+ * @param {Element} container Focusable element.
+ * @param {boolean} isReverse Set to true to check top, false for bottom.
+ *
+ * @return {boolean} True if at the vertical edge, false if not.
+ */
+export function isVerticalEdge( container, isReverse ) {
+	return isEdge( container, isReverse, true );
 }
 
 /**
@@ -228,6 +188,18 @@ export function getRectangleFromRange( range ) {
 		return range.getBoundingClientRect();
 	}
 
+	const { startContainer } = range;
+
+	// Correct invalid "BR" ranges. The cannot contain any children.
+	if ( startContainer.nodeName === 'BR' ) {
+		const { parentNode } = startContainer;
+		const index = Array.from( parentNode.childNodes ).indexOf( startContainer );
+
+		range = document.createRange();
+		range.setStart( parentNode, index );
+		range.setEnd( parentNode, index );
+	}
+
 	let rect = range.getClientRects()[ 0 ];
 
 	// If the collapsed range starts (and therefore ends) at an element node,
diff --git a/packages/e2e-tests/specs/__snapshots__/adding-blocks.test.js.snap b/packages/e2e-tests/specs/__snapshots__/adding-blocks.test.js.snap
index 30b83349a7030a..b8160cb3842efc 100644
--- a/packages/e2e-tests/specs/__snapshots__/adding-blocks.test.js.snap
+++ b/packages/e2e-tests/specs/__snapshots__/adding-blocks.test.js.snap
@@ -42,7 +42,9 @@ exports[`adding blocks Should insert content using the placeholder and the regul
 <!-- /wp:quote -->
 
 <!-- wp:preformatted -->
-<pre class=\\"wp-block-preformatted\\">Pre text<br><br>Foo</pre>
+<pre class=\\"wp-block-preformatted\\">Pre text
+
+Foo</pre>
 <!-- /wp:preformatted -->
 
 <!-- wp:shortcode -->
diff --git a/packages/e2e-tests/specs/__snapshots__/multi-block-selection.test.js.snap b/packages/e2e-tests/specs/__snapshots__/multi-block-selection.test.js.snap
index 6986ffc94968bc..f68e40f606dc4c 100644
--- a/packages/e2e-tests/specs/__snapshots__/multi-block-selection.test.js.snap
+++ b/packages/e2e-tests/specs/__snapshots__/multi-block-selection.test.js.snap
@@ -6,6 +6,8 @@ exports[`Multi-block selection should allow selecting outer edge if there is no
 <!-- /wp:paragraph -->"
 `;
 
+exports[`Multi-block selection should always expand single line selection 1`] = `""`;
+
 exports[`Multi-block selection should only trigger multi-selection when at the end 1`] = `
 "<!-- wp:paragraph -->
 <p>1.</p>
diff --git a/packages/e2e-tests/specs/__snapshots__/rich-text.test.js.snap b/packages/e2e-tests/specs/__snapshots__/rich-text.test.js.snap
index f5c79ddbd33ac5..bf5fefd31a7e32 100644
--- a/packages/e2e-tests/specs/__snapshots__/rich-text.test.js.snap
+++ b/packages/e2e-tests/specs/__snapshots__/rich-text.test.js.snap
@@ -18,6 +18,12 @@ exports[`RichText should apply formatting with primary shortcut 1`] = `
 <!-- /wp:paragraph -->"
 `;
 
+exports[`RichText should apply multiple formats when selection is collapsed 1`] = `
+"<!-- wp:paragraph -->
+<p><strong><em>1</em></strong>.</p>
+<!-- /wp:paragraph -->"
+`;
+
 exports[`RichText should handle change in tag name gracefully 1`] = `
 "<!-- wp:heading {\\"level\\":3} -->
 <h3></h3>
diff --git a/packages/e2e-tests/specs/__snapshots__/writing-flow.test.js.snap b/packages/e2e-tests/specs/__snapshots__/writing-flow.test.js.snap
index 41f6fe04a2b531..2a80f20633a953 100644
--- a/packages/e2e-tests/specs/__snapshots__/writing-flow.test.js.snap
+++ b/packages/e2e-tests/specs/__snapshots__/writing-flow.test.js.snap
@@ -126,6 +126,16 @@ exports[`adding blocks should navigate around nested inline boundaries 2`] = `
 <!-- /wp:paragraph -->"
 `;
 
+exports[`adding blocks should navigate empty paragraph 1`] = `
+"<!-- wp:paragraph -->
+<p>1</p>
+<!-- /wp:paragraph -->
+
+<!-- wp:paragraph -->
+<p>2</p>
+<!-- /wp:paragraph -->"
+`;
+
 exports[`adding blocks should not create extra line breaks in multiline value 1`] = `
 "<!-- wp:quote -->
 <blockquote class=\\"wp-block-quote\\"><p></p></blockquote>
diff --git a/packages/e2e-tests/specs/blocks/__snapshots__/preformatted.test.js.snap b/packages/e2e-tests/specs/blocks/__snapshots__/preformatted.test.js.snap
index 4538816e15b411..07a70a19a6b2be 100644
--- a/packages/e2e-tests/specs/blocks/__snapshots__/preformatted.test.js.snap
+++ b/packages/e2e-tests/specs/blocks/__snapshots__/preformatted.test.js.snap
@@ -9,6 +9,8 @@ exports[`Preformatted should preserve character newlines 1`] = `
 
 exports[`Preformatted should preserve character newlines 2`] = `
 "<!-- wp:preformatted -->
-<pre class=\\"wp-block-preformatted\\">0<br>1<br>2</pre>
+<pre class=\\"wp-block-preformatted\\">0
+1
+2</pre>
 <!-- /wp:preformatted -->"
 `;
diff --git a/packages/e2e-tests/specs/embedding.test.js b/packages/e2e-tests/specs/embedding.test.js
index 3ea51358b14b21..6fb26c02281c70 100644
--- a/packages/e2e-tests/specs/embedding.test.js
+++ b/packages/e2e-tests/specs/embedding.test.js
@@ -9,6 +9,8 @@ import {
 	createJSONResponse,
 	getEditedPostContent,
 	clickButton,
+	insertBlock,
+	publishPost,
 } from '@wordpress/e2e-test-utils';
 
 const MOCK_EMBED_WORDPRESS_SUCCESS_RESPONSE = {
@@ -60,6 +62,10 @@ const MOCK_BAD_WORDPRESS_RESPONSE = {
 };
 
 const MOCK_RESPONSES = [
+	{
+		match: createEmbeddingMatcher( 'https://wordpress.org/gutenberg/handbook' ),
+		onRequestMatch: createJSONResponse( MOCK_BAD_WORDPRESS_RESPONSE ),
+	},
 	{
 		match: createEmbeddingMatcher( 'https://wordpress.org/gutenberg/handbook/' ),
 		onRequestMatch: createJSONResponse( MOCK_BAD_WORDPRESS_RESPONSE ),
@@ -80,6 +86,10 @@ const MOCK_RESPONSES = [
 		match: createEmbeddingMatcher( 'https://twitter.com/notnownikki' ),
 		onRequestMatch: createJSONResponse( MOCK_EMBED_RICH_SUCCESS_RESPONSE ),
 	},
+	{
+		match: createEmbeddingMatcher( 'https://twitter.com/notnownikki/' ),
+		onRequestMatch: createJSONResponse( MOCK_CANT_EMBED_RESPONSE ),
+	},
 	{
 		match: createEmbeddingMatcher( 'https://twitter.com/thatbunty' ),
 		onRequestMatch: createJSONResponse( MOCK_BAD_EMBED_PROVIDER_RESPONSE ),
@@ -173,6 +183,17 @@ describe( 'Embedding content', () => {
 		expect( await getEditedPostContent() ).toMatchSnapshot();
 	} );
 
+	it( 'should retry embeds that could not be embedded with trailing slashes, without the trailing slashes', async () => {
+		await clickBlockAppender();
+		await page.keyboard.type( '/embed' );
+		await page.keyboard.press( 'Enter' );
+		// This URL can't be embedded, but without the trailing slash, it can.
+		await page.keyboard.type( 'https://twitter.com/notnownikki/' );
+		await page.keyboard.press( 'Enter' );
+		// The twitter block should appear correctly.
+		await page.waitForSelector( 'figure.wp-block-embed-twitter' );
+	} );
+
 	it( 'should allow the user to try embedding a failed URL again', async () => {
 		// URL that can't be embedded.
 		await clickBlockAppender();
@@ -192,4 +213,27 @@ describe( 'Embedding content', () => {
 		await clickButton( 'Try again' );
 		await page.waitForSelector( 'figure.wp-block-embed-twitter' );
 	} );
+
+	it( 'should switch to the WordPress block correctly', async () => {
+		// This test is to make sure that WordPress embeds are detected correctly,
+		// because the HTML can vary, and the block is detected by looking for
+		// classes in the HTML, so we need to flag up if the HTML changes.
+
+		// Publish a post to embed.
+		await insertBlock( 'Paragraph' );
+		await page.keyboard.type( 'Hello there!' );
+		await publishPost();
+		const postUrl = await page.$eval( '#inspector-text-control-0', ( el ) => el.value );
+
+		// Start a new post, embed the previous post.
+		await createNewPost();
+		await clickBlockAppender();
+		await page.keyboard.type( '/embed' );
+		await page.keyboard.press( 'Enter' );
+		await page.keyboard.type( postUrl );
+		await page.keyboard.press( 'Enter' );
+
+		// Check the block has become a WordPress block.
+		await page.waitForSelector( '.wp-block-embed-wordpress' );
+	} );
 } );
diff --git a/packages/e2e-tests/specs/multi-block-selection.test.js b/packages/e2e-tests/specs/multi-block-selection.test.js
index 76ea211367660d..6bf7a62bf7789e 100644
--- a/packages/e2e-tests/specs/multi-block-selection.test.js
+++ b/packages/e2e-tests/specs/multi-block-selection.test.js
@@ -181,6 +181,19 @@ describe( 'Multi-block selection', () => {
 		expect( await getEditedPostContent() ).toMatchSnapshot();
 	} );
 
+	it( 'should always expand single line selection', async () => {
+		await clickBlockAppender();
+		await page.keyboard.press( 'Enter' );
+		await page.keyboard.type( '12' );
+		await page.keyboard.press( 'ArrowLeft' );
+		await pressKeyWithModifier( 'shift', 'ArrowRight' );
+		await pressKeyWithModifier( 'shift', 'ArrowUp' );
+		// This delete all blocks.
+		await page.keyboard.press( 'Backspace' );
+
+		expect( await getEditedPostContent() ).toMatchSnapshot();
+	} );
+
 	it( 'should allow selecting outer edge if there is no sibling block', async () => {
 		await clickBlockAppender();
 		await page.keyboard.type( '1' );
diff --git a/packages/e2e-tests/specs/rich-text.test.js b/packages/e2e-tests/specs/rich-text.test.js
index 5153032953899e..dcb1434abd89be 100644
--- a/packages/e2e-tests/specs/rich-text.test.js
+++ b/packages/e2e-tests/specs/rich-text.test.js
@@ -58,6 +58,34 @@ describe( 'RichText', () => {
 		expect( await getEditedPostContent() ).toMatchSnapshot();
 	} );
 
+	it( 'should apply multiple formats when selection is collapsed', async () => {
+		await clickBlockAppender();
+		await pressKeyWithModifier( 'primary', 'b' );
+		await pressKeyWithModifier( 'primary', 'i' );
+		await page.keyboard.type( '1' );
+		await pressKeyWithModifier( 'primary', 'i' );
+		await pressKeyWithModifier( 'primary', 'b' );
+		await page.keyboard.type( '.' );
+
+		expect( await getEditedPostContent() ).toMatchSnapshot();
+	} );
+
+	it( 'should not highlight more than one format', async () => {
+		await clickBlockAppender();
+		await pressKeyWithModifier( 'primary', 'b' );
+		await page.keyboard.type( '1' );
+		await pressKeyWithModifier( 'primary', 'b' );
+		await page.keyboard.type( ' 2' );
+		await pressKeyWithModifier( 'shift', 'ArrowLeft' );
+		await pressKeyWithModifier( 'primary', 'b' );
+
+		const count = await page.evaluate( () => document.querySelectorAll(
+			'*[data-rich-text-format-boundary]'
+		).length );
+
+		expect( count ).toBe( 1 );
+	} );
+
 	it( 'should return focus when pressing formatting button', async () => {
 		await clickBlockAppender();
 		await page.keyboard.type( 'Some ' );
diff --git a/packages/e2e-tests/specs/writing-flow.test.js b/packages/e2e-tests/specs/writing-flow.test.js
index 62ece158016405..c186507a7cdef3 100644
--- a/packages/e2e-tests/specs/writing-flow.test.js
+++ b/packages/e2e-tests/specs/writing-flow.test.js
@@ -298,4 +298,16 @@ describe( 'adding blocks', () => {
 		// Check that none of the paragraph blocks have <br> in them.
 		expect( await getEditedPostContent() ).toMatchSnapshot();
 	} );
+
+	it( 'should navigate empty paragraph', async () => {
+		await clickBlockAppender();
+		await page.keyboard.press( 'Enter' );
+		await page.waitForFunction( () => document.activeElement.isContentEditable );
+		await page.keyboard.press( 'ArrowLeft' );
+		await page.keyboard.type( '1' );
+		await page.keyboard.press( 'ArrowRight' );
+		await page.keyboard.type( '2' );
+
+		expect( await getEditedPostContent() ).toMatchSnapshot();
+	} );
 } );
diff --git a/packages/edit-post/CHANGELOG.md b/packages/edit-post/CHANGELOG.md
index db8c68da542295..4e58838aade5e6 100644
--- a/packages/edit-post/CHANGELOG.md
+++ b/packages/edit-post/CHANGELOG.md
@@ -5,7 +5,9 @@
 * Expose the `className` property to style the `PluginSidebar` component.
 
 ### Bug Fixes
- - Fix 'save' keyboard shortcut not functioning in the Code Editor.
+
+- Fix 'save' keyboard shortcut not functioning in the Code Editor.
+- Prevent `ClipboardButton` from incorrectly copying a serialized block string instead of the intended text in Safari.
 
 ## 3.1.7 (2019-01-03)
 
diff --git a/packages/edit-post/src/components/block-settings-menu/plugin-block-settings-menu-item.js b/packages/edit-post/src/components/block-settings-menu/plugin-block-settings-menu-item.js
index 810d558346cc79..7a7451a983ce3b 100644
--- a/packages/edit-post/src/components/block-settings-menu/plugin-block-settings-menu-item.js
+++ b/packages/edit-post/src/components/block-settings-menu/plugin-block-settings-menu-item.js
@@ -6,7 +6,7 @@ import { difference } from 'lodash';
 /**
  * WordPress dependencies
  */
-import { IconButton } from '@wordpress/components';
+import { MenuItem } from '@wordpress/components';
 import { compose } from '@wordpress/compose';
 
 /**
@@ -89,7 +89,7 @@ const PluginBlockSettingsMenuItem = ( { allowedBlocks, icon, label, onClick, sma
 			if ( ! shouldRenderItem( selectedBlocks, allowedBlocks ) ) {
 				return null;
 			}
-			return ( <IconButton
+			return ( <MenuItem
 				className="editor-block-settings-menu__control"
 				onClick={ compose( onClick, onClose ) }
 				icon={ icon || 'admin-plugins' }
@@ -97,7 +97,7 @@ const PluginBlockSettingsMenuItem = ( { allowedBlocks, icon, label, onClick, sma
 				role={ role }
 			>
 				{ ! small && label }
-			</IconButton> );
+			</MenuItem> );
 		} }
 	</PluginBlockSettingsMenuGroup>
 );
diff --git a/packages/edit-post/src/components/header/feature-toggle/index.js b/packages/edit-post/src/components/header/feature-toggle/index.js
index 4e0df7f3749b08..b855ca046c3866 100644
--- a/packages/edit-post/src/components/header/feature-toggle/index.js
+++ b/packages/edit-post/src/components/header/feature-toggle/index.js
@@ -26,7 +26,6 @@ function FeatureToggle( { onToggle, isActive, label, info, messageActivated, mes
 			isSelected={ isActive }
 			onClick={ flow( onToggle, speakMessage ) }
 			role="menuitemcheckbox"
-			label={ label }
 			info={ info }
 		>
 			{ label }
diff --git a/packages/edit-post/src/components/header/plugin-more-menu-item/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/header/plugin-more-menu-item/test/__snapshots__/index.js.snap
index 6e395b2e7ebae9..7fa0869a60f223 100644
--- a/packages/edit-post/src/components/header/plugin-more-menu-item/test/__snapshots__/index.js.snap
+++ b/packages/edit-post/src/components/header/plugin-more-menu-item/test/__snapshots__/index.js.snap
@@ -18,6 +18,7 @@ exports[`PluginMoreMenuItem renders menu item as button properly 1`] = `
   >
     <button
       className="components-button components-icon-button components-menu-item__button has-icon has-text"
+      instanceId={0}
       onClick={[Function]}
       role="menuitem"
       type="button"
diff --git a/packages/edit-post/src/components/sidebar/style.scss b/packages/edit-post/src/components/sidebar/style.scss
index 793088a0390911..62936b0f0a7ff6 100644
--- a/packages/edit-post/src/components/sidebar/style.scss
+++ b/packages/edit-post/src/components/sidebar/style.scss
@@ -40,17 +40,10 @@
 		z-index: z-index(".edit-post-sidebar .components-panel");
 
 		@include break-small() {
-			overflow: inherit;
+			overflow: hidden;
 			height: auto;
 			max-height: none;
 		}
-
-		@include break-medium() {
-
-			body.is-fullscreen-mode & {
-				max-height: calc(100vh - #{ $panel-header-height });
-			}
-		}
 	}
 
 	> .components-panel .components-panel__header {
diff --git a/packages/edit-post/src/components/visual-editor/block-inspector-button.js b/packages/edit-post/src/components/visual-editor/block-inspector-button.js
index 283c3f99b86a70..cf73fe27de92f7 100644
--- a/packages/edit-post/src/components/visual-editor/block-inspector-button.js
+++ b/packages/edit-post/src/components/visual-editor/block-inspector-button.js
@@ -39,7 +39,6 @@ export function BlockInspectorButton( {
 			className="editor-block-settings-menu__control block-editor-block-settings-menu__control"
 			onClick={ flow( areAdvancedSettingsOpened ? closeSidebar : openEditorSidebar, speakMessage, onClick ) }
 			icon="admin-generic"
-			label={ small ? label : undefined }
 			shortcut={ shortcuts.toggleSidebar }
 		>
 			{ ! small && label }
diff --git a/packages/edit-post/src/components/visual-editor/index.js b/packages/edit-post/src/components/visual-editor/index.js
index eadf8a5fc461df..ca6a1c45212b6b 100644
--- a/packages/edit-post/src/components/visual-editor/index.js
+++ b/packages/edit-post/src/components/visual-editor/index.js
@@ -26,12 +26,13 @@ function VisualEditor() {
 	return (
 		<BlockSelectionClearer className="edit-post-visual-editor editor-styles-wrapper">
 			<VisualEditorGlobalKeyboardShortcuts />
-			<CopyHandler />
 			<MultiSelectScrollIntoView />
 			<WritingFlow>
 				<ObserveTyping>
-					<PostTitle />
-					<BlockList />
+					<CopyHandler>
+						<PostTitle />
+						<BlockList />
+					</CopyHandler>
 				</ObserveTyping>
 			</WritingFlow>
 			<_BlockSettingsMenuFirstItem>
diff --git a/packages/rich-text/src/apply-format.js b/packages/rich-text/src/apply-format.js
index c9a59b96e6a656..14402735b1e60c 100644
--- a/packages/rich-text/src/apply-format.js
+++ b/packages/rich-text/src/apply-format.js
@@ -28,7 +28,8 @@ export function applyFormat(
 	startIndex = value.start,
 	endIndex = value.end
 ) {
-	const newFormats = value.formats.slice( 0 );
+	const { formats, activeFormats = [] } = value;
+	const newFormats = formats.slice();
 
 	// The selection is collapsed.
 	if ( startIndex === endIndex ) {
@@ -51,11 +52,9 @@ export function applyFormat(
 		// Otherwise, insert a placeholder with the format so new input appears
 		// with the format applied.
 		} else {
-			const previousFormat = newFormats[ startIndex - 1 ] || [];
-
 			return {
 				...value,
-				formatPlaceholder: [ ...previousFormat, format ],
+				activeFormats: [ ...activeFormats, format ],
 			};
 		}
 	} else {
diff --git a/packages/rich-text/src/get-active-formats.js b/packages/rich-text/src/get-active-formats.js
index fd0f869877bfb1..47ec6b56fea975 100644
--- a/packages/rich-text/src/get-active-formats.js
+++ b/packages/rich-text/src/get-active-formats.js
@@ -5,19 +5,29 @@
  *
  * @return {?Object} Active format objects.
  */
-export function getActiveFormats( { formats, start, selectedFormat } ) {
+export function getActiveFormats( { formats, start, end, activeFormats } ) {
 	if ( start === undefined ) {
 		return [];
 	}
 
-	const formatsBefore = formats[ start - 1 ] || [];
-	const formatsAfter = formats[ start ] || [];
+	if ( start === end ) {
+		// For a collapsed caret, it is possible to override the active formats.
+		if ( activeFormats ) {
+			return activeFormats;
+		}
 
-	let source = formatsAfter;
+		const formatsBefore = formats[ start - 1 ] || [];
+		const formatsAfter = formats[ start ] || [];
 
-	if ( formatsBefore.length > formatsAfter.length ) {
-		source = formatsBefore;
+		// By default, select the lowest amount of formats possible (which means
+		// the caret is positioned outside the format boundary). The user can
+		// then use arrow keys to define `activeFormats`.
+		if ( formatsBefore.length < formatsAfter.length ) {
+			return formatsBefore;
+		}
+
+		return formatsAfter;
 	}
 
-	return source.slice( 0, selectedFormat );
+	return formats[ start ] || [];
 }
diff --git a/packages/rich-text/src/index.js b/packages/rich-text/src/index.js
index 9d9f4620212876..63da636257619d 100644
--- a/packages/rich-text/src/index.js
+++ b/packages/rich-text/src/index.js
@@ -33,3 +33,5 @@ export { unregisterFormatType } from './unregister-format-type';
 export { indentListItems } from './indent-list-items';
 export { outdentListItems } from './outdent-list-items';
 export { changeListType } from './change-list-type';
+export { updateFormats as __unstableUpdateFormats } from './update-formats';
+export { getActiveFormats as __unstableGetActiveFormats } from './get-active-formats';
diff --git a/packages/rich-text/src/normalise-formats.js b/packages/rich-text/src/normalise-formats.js
index 72c04f818f9a4f..460388e0a74cbf 100644
--- a/packages/rich-text/src/normalise-formats.js
+++ b/packages/rich-text/src/normalise-formats.js
@@ -1,9 +1,3 @@
-/**
- * External dependencies
- */
-
-import { find } from 'lodash';
-
 /**
  * Internal dependencies
  */
@@ -11,29 +5,36 @@ import { find } from 'lodash';
 import { isFormatEqual } from './is-format-equal';
 
 /**
- * Normalises formats: ensures subsequent equal formats have the same reference.
+ * Normalises formats: ensures subsequent adjacent equal formats have the same
+ * reference.
  *
  * @param {Object} value Value to normalise formats of.
  *
  * @return {Object} New value with normalised formats.
  */
 export function normaliseFormats( value ) {
-	const refs = [];
-	const newFormats = value.formats.map( ( formatsAtIndex ) =>
-		formatsAtIndex.map( ( format ) => {
-			const equalRef = find( refs, ( ref ) =>
-				isFormatEqual( ref, format )
-			);
+	const newFormats = value.formats.slice();
+
+	newFormats.forEach( ( formatsAtIndex, index ) => {
+		const formatsAtPreviousIndex = newFormats[ index - 1 ];
+
+		if ( formatsAtPreviousIndex ) {
+			const newFormatsAtIndex = formatsAtIndex.slice();
 
-			if ( equalRef ) {
-				return equalRef;
-			}
+			newFormatsAtIndex.forEach( ( format, formatIndex ) => {
+				const previousFormat = formatsAtPreviousIndex[ formatIndex ];
 
-			refs.push( format );
+				if ( isFormatEqual( format, previousFormat ) ) {
+					newFormatsAtIndex[ formatIndex ] = previousFormat;
+				}
+			} );
 
-			return format;
-		} )
-	);
+			newFormats[ index ] = newFormatsAtIndex;
+		}
+	} );
 
-	return { ...value, formats: newFormats };
+	return {
+		...value,
+		formats: newFormats,
+	};
 }
diff --git a/packages/rich-text/src/remove-format.js b/packages/rich-text/src/remove-format.js
index 405cb6265e1e06..4a4c9c820f9008 100644
--- a/packages/rich-text/src/remove-format.js
+++ b/packages/rich-text/src/remove-format.js
@@ -28,7 +28,8 @@ export function removeFormat(
 	startIndex = value.start,
 	endIndex = value.end
 ) {
-	const newFormats = value.formats.slice( 0 );
+	const { formats, activeFormats } = value;
+	const newFormats = formats.slice();
 
 	// If the selection is collapsed, expand start and end to the edges of the
 	// format.
@@ -50,10 +51,7 @@ export function removeFormat(
 		} else {
 			return {
 				...value,
-				formatPlaceholder: reject(
-					newFormats[ startIndex - 1 ] || [],
-					{ type: formatType }
-				),
+				activeFormats: reject( activeFormats, { type: formatType } ),
 			};
 		}
 	} else {
diff --git a/packages/rich-text/src/test/apply-format.js b/packages/rich-text/src/test/apply-format.js
index b8cff3bc476202..b75dc7ee5de008 100644
--- a/packages/rich-text/src/test/apply-format.js
+++ b/packages/rich-text/src/test/apply-format.js
@@ -61,7 +61,7 @@ describe( 'applyFormat', () => {
 		};
 		const expected = {
 			...record,
-			formatPlaceholder: [ a2 ],
+			activeFormats: [ a2 ],
 		};
 		const result = applyFormat( deepFreeze( record ), a2 );
 
diff --git a/packages/rich-text/src/test/get-active-format.js b/packages/rich-text/src/test/get-active-format.js
index 4fa0ddaa3e3ec6..eba352783c39ab 100644
--- a/packages/rich-text/src/test/get-active-format.js
+++ b/packages/rich-text/src/test/get-active-format.js
@@ -6,39 +6,82 @@ import { getActiveFormat } from '../get-active-format';
 
 describe( 'getActiveFormat', () => {
 	const em = { type: 'em' };
+	const strong = { type: 'strong' };
 
-	it( 'should get format by selection', () => {
+	it( 'should return undefined if there is no selection', () => {
 		const record = {
-			formats: [ [ em ], , , ],
+			formats: [ [ em ], [ em ], [ em ] ],
+			text: 'one',
+		};
+
+		expect( getActiveFormat( record, 'em' ) ).toBe( undefined );
+	} );
+
+	it( 'should return format at first character for uncollapsed selection', () => {
+		const record = {
+			formats: [ [ em ], [ strong ], , ],
 			text: 'one',
 			start: 0,
-			end: 0,
+			end: 2,
 		};
 
-		expect( getActiveFormat( record, 'em' ) ).toEqual( em );
+		expect( getActiveFormat( record, 'em' ) ).toBe( em );
 	} );
 
-	it( 'should not get any format if outside boundary position', () => {
+	it( 'should return undefined if at the boundary before', () => {
+		const record = {
+			formats: [ [ em ], , [ em ] ],
+			text: 'one',
+			start: 3,
+			end: 3,
+		};
+
+		expect( getActiveFormat( record, 'em' ) ).toBe( undefined );
+	} );
+
+	it( 'should return undefined if at the boundary after', () => {
 		const record = {
 			formats: [ [ em ], , [ em ] ],
 			text: 'one',
 			start: 1,
 			end: 1,
-			selectedFormat: 0,
 		};
 
 		expect( getActiveFormat( record, 'em' ) ).toBe( undefined );
 	} );
 
-	it( 'should get format if inside boundary position', () => {
+	it( 'should return format if inside format', () => {
+		const record = {
+			formats: [ [ em ], [ em ], [ em ] ],
+			text: 'one',
+			start: 1,
+			end: 1,
+		};
+
+		expect( getActiveFormat( record, 'em' ) ).toBe( em );
+	} );
+
+	it( 'should return activeFormats', () => {
 		const record = {
 			formats: [ [ em ], , [ em ] ],
 			text: 'one',
 			start: 1,
 			end: 1,
-			selectedFormat: 1,
+			activeFormats: [ em ],
 		};
 
 		expect( getActiveFormat( record, 'em' ) ).toBe( em );
 	} );
+
+	it( 'should not return activeFormats for uncollapsed selection', () => {
+		const record = {
+			formats: [ [ em ], , [ em ] ],
+			text: 'one',
+			start: 1,
+			end: 2,
+			activeFormats: [ em ],
+		};
+
+		expect( getActiveFormat( record, 'em' ) ).toBe( undefined );
+	} );
 } );
diff --git a/packages/rich-text/src/test/normalise-formats.js b/packages/rich-text/src/test/normalise-formats.js
index 25525625f18b5d..c71b8a35ed634d 100644
--- a/packages/rich-text/src/test/normalise-formats.js
+++ b/packages/rich-text/src/test/normalise-formats.js
@@ -26,7 +26,7 @@ describe( 'normaliseFormats', () => {
 		expect( getSparseArrayLength( result.formats ) ).toBe( 4 );
 		expect( result.formats[ 1 ][ 0 ] ).toBe( result.formats[ 2 ][ 0 ] );
 		expect( result.formats[ 1 ][ 0 ] ).toBe( result.formats[ 3 ][ 0 ] );
-		expect( result.formats[ 1 ][ 0 ] ).toBe( result.formats[ 5 ][ 0 ] );
+		expect( result.formats[ 1 ][ 0 ] ).not.toBe( result.formats[ 5 ][ 0 ] );
 		expect( result.formats[ 2 ][ 1 ] ).toBe( result.formats[ 3 ][ 1 ] );
 	} );
 } );
diff --git a/packages/rich-text/src/test/update-formats.js b/packages/rich-text/src/test/update-formats.js
new file mode 100644
index 00000000000000..e5c3c97a85bb77
--- /dev/null
+++ b/packages/rich-text/src/test/update-formats.js
@@ -0,0 +1,55 @@
+/**
+ * Internal dependencies
+ */
+
+import { updateFormats } from '../update-formats';
+import { getSparseArrayLength } from './helpers';
+
+describe( 'updateFormats', () => {
+	const em = { type: 'em' };
+
+	it( 'should update formats with empty array', () => {
+		const value = {
+			formats: [ [ em ] ],
+			text: '1',
+		};
+		const expected = {
+			...value,
+			activeFormats: [],
+			formats: [ , ],
+		};
+		const result = updateFormats( {
+			value,
+			start: 0,
+			end: 1,
+			formats: [],
+		} );
+
+		expect( result ).toEqual( expected );
+		expect( result ).toBe( value );
+		expect( getSparseArrayLength( result.formats ) ).toBe( 0 );
+	} );
+
+	it( 'should update formats and update references', () => {
+		const value = {
+			formats: [ [ em ], , ],
+			text: '123',
+		};
+		const expected = {
+			...value,
+			activeFormats: [ em ],
+			formats: [ [ em ], [ em ] ],
+		};
+		const result = updateFormats( {
+			value,
+			start: 1,
+			end: 2,
+			formats: [ { ...em } ],
+		} );
+
+		expect( result ).toEqual( expected );
+		expect( result ).toBe( value );
+		expect( result.formats[ 1 ][ 0 ] ).toBe( em );
+		expect( getSparseArrayLength( result.formats ) ).toBe( 2 );
+	} );
+} );
diff --git a/packages/rich-text/src/to-tree.js b/packages/rich-text/src/to-tree.js
index ef15fe7524b6ea..c76eafc803b92f 100644
--- a/packages/rich-text/src/to-tree.js
+++ b/packages/rich-text/src/to-tree.js
@@ -69,17 +69,6 @@ function fromFormat( { type, attributes, unregisteredAttributes, object, boundar
 	};
 }
 
-function getDeepestActiveFormat( value ) {
-	const activeFormats = getActiveFormats( value );
-	const { selectedFormat } = value;
-
-	if ( selectedFormat === undefined ) {
-		return activeFormats[ activeFormats.length - 1 ];
-	}
-
-	return activeFormats[ selectedFormat - 1 ];
-}
-
 const padding = {
 	type: 'br',
 	attributes: {
@@ -107,7 +96,8 @@ export function toTree( {
 	const formatsLength = formats.length + 1;
 	const tree = createEmpty();
 	const multilineFormat = { type: multilineTag };
-	const deepestActiveFormat = getDeepestActiveFormat( value );
+	const activeFormats = getActiveFormats( value );
+	const deepestActiveFormat = activeFormats[ activeFormats.length - 1 ];
 
 	let lastSeparatorFormats;
 	let lastCharacterFormats;
diff --git a/packages/rich-text/src/update-formats.js b/packages/rich-text/src/update-formats.js
new file mode 100644
index 00000000000000..bc99c9ba0e5dbe
--- /dev/null
+++ b/packages/rich-text/src/update-formats.js
@@ -0,0 +1,48 @@
+/**
+ * Internal dependencies
+ */
+
+import { isFormatEqual } from './is-format-equal';
+
+/**
+ * Efficiently updates all the formats from `start` (including) until `end`
+ * (excluding) with the active formats. Mutates `value`.
+ *
+ * @param  {Object} $1         Named paramentes.
+ * @param  {Object} $1.value   Value te update.
+ * @param  {number} $1.start   Index to update from.
+ * @param  {number} $1.end     Index to update until.
+ * @param  {Array}  $1.formats Replacement formats.
+ *
+ * @return {Object} Mutated value.
+ */
+export function updateFormats( { value, start, end, formats } ) {
+	const formatsBefore = value.formats[ start - 1 ] || [];
+	const formatsAfter = value.formats[ end ] || [];
+
+	// First, fix the references. If any format right before or after are
+	// equal, the replacement format should use the same reference.
+	value.activeFormats = formats.map( ( format, index ) => {
+		if ( formatsBefore[ index ] ) {
+			if ( isFormatEqual( format, formatsBefore[ index ] ) ) {
+				return formatsBefore[ index ];
+			}
+		} else if ( formatsAfter[ index ] ) {
+			if ( isFormatEqual( format, formatsAfter[ index ] ) ) {
+				return formatsAfter[ index ];
+			}
+		}
+
+		return format;
+	} );
+
+	while ( --end >= start ) {
+		if ( value.activeFormats.length > 0 ) {
+			value.formats[ end ] = value.activeFormats;
+		} else {
+			delete value.formats[ end ];
+		}
+	}
+
+	return value;
+}
diff --git a/test/integration/blocks-raw-handling.spec.js b/test/integration/blocks-raw-handling.spec.js
index 72426e00094fb8..83315ba0d38a68 100644
--- a/test/integration/blocks-raw-handling.spec.js
+++ b/test/integration/blocks-raw-handling.spec.js
@@ -221,6 +221,14 @@ describe( 'Blocks raw handling', () => {
 		expect( console ).toHaveLogged();
 	} );
 
+	it( 'should paste gutenberg content from plain text', () => {
+		const block = '<!-- wp:latest-posts /-->';
+		expect( serialize( pasteHandler( {
+			plainText: block,
+			mode: 'AUTO',
+		} ) ) ).toBe( block );
+	} );
+
 	describe( 'pasteHandler', () => {
 		[
 			'plain',