diff --git a/packages/block-editor/src/components/block-list/block.native.js b/packages/block-editor/src/components/block-list/block.native.js
index d8366273376351..b87f95445e11f2 100644
--- a/packages/block-editor/src/components/block-list/block.native.js
+++ b/packages/block-editor/src/components/block-list/block.native.js
@@ -118,8 +118,6 @@ class BlockListBlock extends Component {
const accessibilityLabel = this.getAccessibilityLabel();
return (
- // accessible prop needs to be false to access children
- // https://facebook.github.io/react-native/docs/accessibility#accessible-ios-android
{ isSelected && }
-
);
}
diff --git a/packages/block-editor/src/components/block-list/index.native.js b/packages/block-editor/src/components/block-list/index.native.js
index 5227bce63ecc67..3a193a9d02a9b1 100644
--- a/packages/block-editor/src/components/block-list/index.native.js
+++ b/packages/block-editor/src/components/block-list/index.native.js
@@ -2,30 +2,26 @@
* External dependencies
*/
import { identity } from 'lodash';
-import { Text, View, Keyboard, SafeAreaView, Platform, TouchableWithoutFeedback } from 'react-native';
-import { subscribeMediaAppend } from 'react-native-gutenberg-bridge';
+import { Text, View, Platform, TouchableWithoutFeedback } from 'react-native';
/**
* WordPress dependencies
*/
-import { Component, Fragment } from '@wordpress/element';
+import { Component } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { withDispatch, withSelect } from '@wordpress/data';
import { compose } from '@wordpress/compose';
import { createBlock, isUnmodifiedDefaultBlock } from '@wordpress/blocks';
-import { HTMLTextInput, KeyboardAvoidingView, KeyboardAwareFlatList, ReadableContentView } from '@wordpress/components';
+import { KeyboardAwareFlatList, ReadableContentView } from '@wordpress/components';
/**
* Internal dependencies
*/
import styles from './style.scss';
import BlockListBlock from './block';
-import BlockToolbar from '../block-toolbar';
import DefaultBlockAppender from '../default-block-appender';
-import Inserter from '../inserter';
-const blockMobileToolbarHeight = 44;
-const toolbarHeight = 44;
+const innerToolbarHeight = 44;
export class BlockList extends Component {
constructor() {
@@ -34,34 +30,11 @@ export class BlockList extends Component {
this.renderItem = this.renderItem.bind( this );
this.renderAddBlockSeparator = this.renderAddBlockSeparator.bind( this );
this.renderBlockListFooter = this.renderBlockListFooter.bind( this );
- this.shouldFlatListPreventAutomaticScroll = this.shouldFlatListPreventAutomaticScroll.bind( this );
this.renderDefaultBlockAppender = this.renderDefaultBlockAppender.bind( this );
- this.onBlockTypeSelected = this.onBlockTypeSelected.bind( this );
- this.keyboardDidShow = this.keyboardDidShow.bind( this );
- this.keyboardDidHide = this.keyboardDidHide.bind( this );
this.onCaretVerticalPositionChange = this.onCaretVerticalPositionChange.bind( this );
this.scrollViewInnerRef = this.scrollViewInnerRef.bind( this );
this.getNewBlockInsertionIndex = this.getNewBlockInsertionIndex.bind( this );
-
- this.state = {
- blockTypePickerVisible: false,
- isKeyboardVisible: false,
- };
- }
-
- // TODO: in the near future this will likely be changed to onShowBlockTypePicker and bound to this.props
- // once we move the action to the toolbar
- showBlockTypePicker( show ) {
- this.setState( { blockTypePickerVisible: show } );
- }
-
- onBlockTypeSelected( itemValue ) {
- this.setState( { blockTypePickerVisible: false } );
-
- // create an empty block of the selected type
- const newBlock = createBlock( itemValue );
-
- this.finishBlockAppendingOrReplacing( newBlock );
+ this.shouldFlatListPreventAutomaticScroll = this.shouldFlatListPreventAutomaticScroll.bind( this );
}
finishBlockAppendingOrReplacing( newBlock ) {
@@ -88,48 +61,7 @@ export class BlockList extends Component {
}
blockHolderBorderStyle() {
- return this.state.isFullyBordered ? styles.blockHolderFullBordered : styles.blockHolderSemiBordered;
- }
-
- componentDidMount() {
- this._isMounted = true;
- Keyboard.addListener( 'keyboardDidShow', this.keyboardDidShow );
- Keyboard.addListener( 'keyboardDidHide', this.keyboardDidHide );
-
- this.subscriptionParentMediaAppend = subscribeMediaAppend( ( payload ) => {
- // create an empty media block
- const newMediaBlock = createBlock( 'core/' + payload.mediaType );
-
- // now set the url and id
- if ( payload.mediaType === 'image' ) {
- newMediaBlock.attributes.url = payload.mediaUrl;
- } else if ( payload.mediaType === 'video' ) {
- newMediaBlock.attributes.src = payload.mediaUrl;
- }
-
- newMediaBlock.attributes.id = payload.mediaId;
-
- // finally append or replace as appropriate
- this.finishBlockAppendingOrReplacing( newMediaBlock );
- } );
- }
-
- componentWillUnmount() {
- Keyboard.removeListener( 'keyboardDidShow', this.keyboardDidShow );
- Keyboard.removeListener( 'keyboardDidHide', this.keyboardDidHide );
-
- if ( this.subscriptionParentMediaAppend ) {
- this.subscriptionParentMediaAppend.remove();
- }
- this._isMounted = false;
- }
-
- keyboardDidShow() {
- this.setState( { isKeyboardVisible: true } );
- }
-
- keyboardDidHide() {
- this.setState( { isKeyboardVisible: false } );
+ return this.props.isFullyBordered ? styles.blockHolderFullBordered : styles.blockHolderSemiBordered;
}
onCaretVerticalPositionChange( targetId, caretY, previousCaretY ) {
@@ -141,7 +73,7 @@ export class BlockList extends Component {
}
shouldFlatListPreventAutomaticScroll() {
- return this.state.blockTypePickerVisible;
+ return this.props.isBlockInsertionPointVisible;
}
renderDefaultBlockAppender() {
@@ -159,7 +91,7 @@ export class BlockList extends Component {
);
}
- renderList() {
+ render() {
return (
-
-
-
-
- {
- this.showBlockTypePicker( true );
- } }
- showKeyboardHideButton={ this.state.isKeyboardVisible }
- />
-
);
}
- render() {
- return (
-
- { this.renderList() }
- { this.state.blockTypePickerVisible && (
- this.showBlockTypePicker( false ) }
- onValueSelected={ this.onBlockTypeSelected }
- isReplacement={ this.isReplaceable( this.props.selectedBlock ) }
- addExtraBottomPadding={ this.props.safeAreaBottomInset === 0 }
- />
- ) }
-
- );
- }
-
isReplaceable( block ) {
if ( ! block ) {
return false;
@@ -226,12 +125,10 @@ export class BlockList extends Component {
return isUnmodifiedDefaultBlock( block );
}
- renderItem( { item: clientId, index } ) {
- const shouldShowAddBlockSeparator = this.state.blockTypePickerVisible && ( this.props.isBlockSelected( clientId ) || ( index === 0 && this.props.isPostTitleSelected ) );
- const shouldPutAddBlockSeparatorAboveBlock = this.isReplaceable( this.props.selectedBlock ) || this.props.isPostTitleSelected;
-
+ renderItem( { item: clientId } ) {
return (
-
+
+ { this.props.shouldShowInsertionPoint( clientId ) && this.renderAddBlockSeparator() }
- { shouldShowAddBlockSeparator && this.renderAddBlockSeparator() }
);
}
@@ -266,12 +162,6 @@ export class BlockList extends Component {
);
}
-
- renderHTML() {
- return (
-
- );
- }
}
export default compose( [
@@ -284,15 +174,28 @@ export default compose( [
getSelectedBlock,
getSelectedBlockClientId,
isBlockSelected,
+ getBlockInsertionPoint,
+ isBlockInsertionPointVisible,
} = select( 'core/block-editor' );
const selectedBlockClientId = getSelectedBlockClientId();
+ const blockClientIds = getBlockOrder( rootClientId );
+ const insertionPoint = getBlockInsertionPoint();
+ const shouldShowInsertionPoint = ( clientId ) => {
+ return (
+ isBlockInsertionPointVisible() &&
+ insertionPoint.rootClientId === rootClientId &&
+ blockClientIds[ insertionPoint.index ] === clientId
+ );
+ };
return {
- blockClientIds: getBlockOrder( rootClientId ),
+ blockClientIds,
blockCount: getBlockCount( rootClientId ),
getBlockName,
isBlockSelected,
+ isBlockInsertionPointVisible: isBlockInsertionPointVisible(),
+ shouldShowInsertionPoint,
selectedBlock: getSelectedBlock(),
selectedBlockClientId,
selectedBlockOrder: getBlockIndex( selectedBlockClientId ),
diff --git a/packages/block-editor/src/components/block-list/style.native.scss b/packages/block-editor/src/components/block-list/style.native.scss
index 884c0612f05867..5185e2099bf93e 100644
--- a/packages/block-editor/src/components/block-list/style.native.scss
+++ b/packages/block-editor/src/components/block-list/style.native.scss
@@ -40,13 +40,6 @@
background-color: $white;
}
-.blockToolbarKeyboardAvoidingView {
- position: absolute;
- bottom: 0;
- right: 0;
- left: 0;
-}
-
.blockHolderSemiBordered {
border-top-width: 1px;
border-bottom-width: 1px;
@@ -61,7 +54,6 @@
border-right-width: 1px;
}
-
.blockContainerFocused {
background-color: $white;
padding-left: 16;
diff --git a/packages/block-editor/src/components/block-toolbar/index.native.js b/packages/block-editor/src/components/block-toolbar/index.native.js
index 3d12312673bf87..5413e4e1f9cded 100644
--- a/packages/block-editor/src/components/block-toolbar/index.native.js
+++ b/packages/block-editor/src/components/block-toolbar/index.native.js
@@ -1,105 +1,41 @@
/**
- * External dependencies
+ * WordPress dependencies
*/
-import { View, ScrollView, Keyboard, Platform } from 'react-native';
+import { withSelect } from '@wordpress/data';
/**
* WordPress dependencies
*/
-import { Component } from '@wordpress/element';
-import { withSelect, withDispatch } from '@wordpress/data';
-import { compose } from '@wordpress/compose';
-import { Toolbar, ToolbarButton, Dashicon } from '@wordpress/components';
import { BlockFormatControls, BlockControls } from '@wordpress/block-editor';
-import { __ } from '@wordpress/i18n';
-
-/**
- * Internal dependencies
- */
-import styles from './style.scss';
-
-export class BlockToolbar extends Component {
- constructor() {
- super( ...arguments );
- this.onKeyboardHide = this.onKeyboardHide.bind( this );
+export const BlockToolbar = ( { blockClientIds, isValid, mode } ) => {
+ if ( blockClientIds.length === 0 ) {
+ return null;
}
- onKeyboardHide() {
- this.props.clearSelectedBlock();
- if ( Platform.OS === 'android' ) {
- // Avoiding extra blur calls on iOS but still needed for android.
- Keyboard.dismiss();
- }
- }
-
- render() {
- const {
- hasRedo,
- hasUndo,
- redo,
- undo,
- onInsertClick,
- showKeyboardHideButton,
- } = this.props;
-
- return (
-
-
-
- ) }
- onClick={ onInsertClick }
- extraProps={ { hint: __( 'Double tap to add a block' ) } }
- />
-
-
-
+ return (
+ <>
+ { mode === 'visual' && isValid && (
+ <>
-
- { showKeyboardHideButton &&
-
-
-
- }
-
- );
- }
-}
-
-export default compose( [
- withSelect( ( select ) => ( {
- hasRedo: select( 'core/editor' ).hasEditorRedo(),
- hasUndo: select( 'core/editor' ).hasEditorUndo(),
- } ) ),
- withDispatch( ( dispatch ) => ( {
- redo: dispatch( 'core/editor' ).redo,
- undo: dispatch( 'core/editor' ).undo,
- clearSelectedBlock: dispatch( 'core/editor' ).clearSelectedBlock,
- } ) ),
-] )( BlockToolbar );
+ >
+ ) }
+ >
+ );
+};
+
+export default withSelect( ( select ) => {
+ const {
+ getBlockMode,
+ getSelectedBlockClientIds,
+ isBlockValid,
+ } = select( 'core/block-editor' );
+ const blockClientIds = getSelectedBlockClientIds();
+
+ return {
+ blockClientIds,
+ isValid: blockClientIds.length === 1 ? isBlockValid( blockClientIds[ 0 ] ) : null,
+ mode: blockClientIds.length === 1 ? getBlockMode( blockClientIds[ 0 ] ) : null,
+ };
+} )( BlockToolbar );
diff --git a/packages/block-editor/src/components/default-block-appender/index.native.js b/packages/block-editor/src/components/default-block-appender/index.native.js
index 1319fd3fd2ce9f..4f361376f6d581 100644
--- a/packages/block-editor/src/components/default-block-appender/index.native.js
+++ b/packages/block-editor/src/components/default-block-appender/index.native.js
@@ -11,6 +11,7 @@ import { RichText } from '@wordpress/block-editor';
import { compose } from '@wordpress/compose';
import { decodeEntities } from '@wordpress/html-entities';
import { withSelect, withDispatch } from '@wordpress/data';
+import { getDefaultBlockName } from '@wordpress/blocks';
/**
* Internal dependencies
@@ -28,7 +29,7 @@ export function DefaultBlockAppender( {
return null;
}
- const value = decodeEntities( placeholder ) || __( 'Start writing…' );
+ const value = typeof placeholder === 'string' ? decodeEntities( placeholder ) : __( 'Start writing…' );
return (
{
- const { getBlockCount, getSettings, getTemplateLock } = select( 'core/block-editor' );
+ const { getBlockCount, getBlockName, isBlockValid, getTemplateLock } = select( 'core/block-editor' );
const isEmpty = ! getBlockCount( ownProps.rootClientId );
- const { bodyPlaceholder } = getSettings();
+ const isLastBlockDefault = getBlockName( ownProps.lastBlockClientId ) === getDefaultBlockName();
+ const isLastBlockValid = isBlockValid( ownProps.lastBlockClientId );
return {
- isVisible: isEmpty,
+ isVisible: isEmpty || ! isLastBlockDefault || ! isLastBlockValid,
isLocked: !! getTemplateLock( ownProps.rootClientId ),
- placeholder: bodyPlaceholder,
};
} ),
withDispatch( ( dispatch, ownProps ) => {
diff --git a/packages/block-editor/src/components/inserter/index.native.js b/packages/block-editor/src/components/inserter/index.native.js
index 2985d61c7811c4..90009022f0bd35 100644
--- a/packages/block-editor/src/components/inserter/index.native.js
+++ b/packages/block-editor/src/components/inserter/index.native.js
@@ -1,12 +1,8 @@
-/**
- * External dependencies
- */
-import { FlatList, Text, TouchableHighlight, View } from 'react-native';
-
/**
* WordPress dependencies
*/
-import { BottomSheet, Icon } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { Dropdown, ToolbarButton, Dashicon } from '@wordpress/components';
import { Component } from '@wordpress/element';
import { withSelect } from '@wordpress/data';
import { compose } from '@wordpress/compose';
@@ -16,58 +12,85 @@ import { getUnregisteredTypeHandlerName } from '@wordpress/blocks';
* Internal dependencies
*/
import styles from './style.scss';
+import InserterMenu from './menu';
+
+const defaultRenderToggle = ( { onToggle, disabled } ) => (
+ ) }
+ onClick={ onToggle }
+ extraProps={ { hint: __( 'Double tap to add a block' ) } }
+ isDisabled={ disabled }
+ />
+);
class Inserter extends Component {
- calculateNumberOfColumns() {
- const bottomSheetWidth = BottomSheet.getWidth();
- const { paddingLeft: itemPaddingLeft, paddingRight: itemPaddingRight } = styles.modalItem;
- const { paddingLeft: containerPaddingLeft, paddingRight: containerPaddingRight } = styles.content;
- const { width: itemWidth } = styles.modalIconWrapper;
- const itemTotalWidth = itemWidth + itemPaddingLeft + itemPaddingRight;
- const containerTotalWidth = bottomSheetWidth - ( containerPaddingLeft + containerPaddingRight );
- return Math.floor( containerTotalWidth / itemTotalWidth );
+ constructor() {
+ super( ...arguments );
+
+ this.onToggle = this.onToggle.bind( this );
+ this.renderToggle = this.renderToggle.bind( this );
+ this.renderContent = this.renderContent.bind( this );
}
- render() {
- const numberOfColumns = this.calculateNumberOfColumns();
- const bottomPadding = this.props.addExtraBottomPadding && styles.contentBottomPadding;
+ onToggle( isOpen ) {
+ const { onToggle } = this.props;
+ // Surface toggle callback to parent component
+ if ( onToggle ) {
+ onToggle( isOpen );
+ }
+ }
+
+ /**
+ * Render callback to display Dropdown toggle element.
+ *
+ * @param {Function} options.onToggle Callback to invoke when toggle is
+ * pressed.
+ * @param {boolean} options.isOpen Whether dropdown is currently open.
+ *
+ * @return {WPElement} Dropdown toggle element.
+ */
+ renderToggle( { onToggle, isOpen } ) {
+ const {
+ disabled,
+ renderToggle = defaultRenderToggle,
+ } = this.props;
+
+ return renderToggle( { onToggle, isOpen, disabled } );
+ }
+
+ /**
+ * Render callback to display Dropdown content element.
+ *
+ * @param {Function} options.onClose Callback to invoke when dropdown is
+ * closed.
+ *
+ * @return {WPElement} Dropdown content element.
+ */
+ renderContent( { onClose, isOpen } ) {
+ const { rootClientId, clientId, isAppender } = this.props;
+
+ return (
+
+ );
+ }
+
+ render() {
return (
-
-
-
- }
- keyExtractor={ ( item ) => item.name }
- renderItem={ ( { item } ) =>
- this.props.onValueSelected( item.name ) }>
-
-
-
-
-
-
- { item.title }
-
-
- }
- />
-
+
);
}
}
diff --git a/packages/block-editor/src/components/inserter/menu.native.js b/packages/block-editor/src/components/inserter/menu.native.js
new file mode 100644
index 00000000000000..bf2dd780841f9c
--- /dev/null
+++ b/packages/block-editor/src/components/inserter/menu.native.js
@@ -0,0 +1,217 @@
+/**
+ * External dependencies
+ */
+import { FlatList, View, Text, TouchableHighlight } from 'react-native';
+import { subscribeMediaAppend } from 'react-native-gutenberg-bridge';
+
+/**
+ * WordPress dependencies
+ */
+import { Component } from '@wordpress/element';
+import {
+ createBlock,
+ isUnmodifiedDefaultBlock,
+} from '@wordpress/blocks';
+import { withDispatch, withSelect } from '@wordpress/data';
+import { withInstanceId, compose } from '@wordpress/compose';
+import { BottomSheet, Icon } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import styles from './style.scss';
+
+export class InserterMenu extends Component {
+ componentDidMount() {
+ this.subscriptionParentMediaAppend = subscribeMediaAppend( ( payload ) => {
+ this.props.onSelect( {
+ name: 'core/' + payload.mediaType,
+ initialAttributes: {
+ id: payload.mediaId,
+ [ payload.mediaType === 'image' ? 'url' : 'src' ]: payload.mediaUrl,
+ },
+ } );
+ } );
+ this.onOpen();
+ }
+
+ componentWillUnmount() {
+ if ( this.subscriptionParentMediaAppend ) {
+ this.subscriptionParentMediaAppend.remove();
+ }
+ this.onClose();
+ }
+
+ calculateNumberOfColumns() {
+ const bottomSheetWidth = BottomSheet.getWidth();
+ const { paddingLeft: itemPaddingLeft, paddingRight: itemPaddingRight } = styles.modalItem;
+ const { paddingLeft: containerPaddingLeft, paddingRight: containerPaddingRight } = styles.content;
+ const { width: itemWidth } = styles.modalIconWrapper;
+ const itemTotalWidth = itemWidth + itemPaddingLeft + itemPaddingRight;
+ const containerTotalWidth = bottomSheetWidth - ( containerPaddingLeft + containerPaddingRight );
+ return Math.floor( containerTotalWidth / itemTotalWidth );
+ }
+
+ onOpen() {
+ this.props.showInsertionPoint();
+ }
+
+ onClose() {
+ this.props.hideInsertionPoint();
+ }
+
+ render() {
+ const numberOfColumns = this.calculateNumberOfColumns();
+ const bottomPadding = styles.contentBottomPadding;
+
+ return (
+
+
+
+ }
+ keyExtractor={ ( item ) => item.name }
+ renderItem={ ( { item } ) =>
+ this.props.onSelect( item ) }>
+
+
+
+
+
+
+ { item.title }
+
+
+ }
+ />
+
+ );
+ /* eslint-enable jsx-a11y/no-autofocus, jsx-a11y/no-noninteractive-element-interactions */
+ }
+}
+
+export default compose(
+ withSelect( ( select, { clientId, isAppender, rootClientId } ) => {
+ const {
+ getInserterItems,
+ getBlockName,
+ getBlockRootClientId,
+ getBlockSelectionEnd,
+ } = select( 'core/block-editor' );
+ const {
+ getChildBlockNames,
+ } = select( 'core/blocks' );
+
+ let destinationRootClientId = rootClientId;
+ if ( ! destinationRootClientId && ! clientId && ! isAppender ) {
+ const end = getBlockSelectionEnd();
+ if ( end ) {
+ destinationRootClientId = getBlockRootClientId( end ) || undefined;
+ }
+ }
+ const destinationRootBlockName = getBlockName( destinationRootClientId );
+
+ return {
+ rootChildBlocks: getChildBlockNames( destinationRootBlockName ),
+ items: getInserterItems( destinationRootClientId ),
+ destinationRootClientId,
+ };
+ } ),
+ withDispatch( ( dispatch, ownProps, { select } ) => {
+ const {
+ showInsertionPoint,
+ hideInsertionPoint,
+ } = dispatch( 'core/block-editor' );
+
+ // To avoid duplication, getInsertionIndex is extracted and used in two event handlers
+ // This breaks the withDispatch not containing any logic rule.
+ // Since it's a function only called when the event handlers are called,
+ // it's fine to extract it.
+ // eslint-disable-next-line no-restricted-syntax
+ function getInsertionIndex() {
+ const {
+ getBlock,
+ getBlockIndex,
+ getBlockSelectionEnd,
+ getBlockOrder,
+ } = select( 'core/block-editor' );
+ const {
+ isPostTitleSelected,
+ } = select( 'core/editor' );
+ const { clientId, destinationRootClientId, isAppender } = ownProps;
+
+ // if post title is selected insert as first block
+ if ( isPostTitleSelected() ) {
+ return 0;
+ }
+
+ // If the clientId is defined, we insert at the position of the block.
+ if ( clientId ) {
+ return getBlockIndex( clientId, destinationRootClientId );
+ }
+
+ // If there a selected block,
+ const end = getBlockSelectionEnd();
+ if ( ! isAppender && end ) {
+ // and the last selected block is unmodified (empty), it will be replaced
+ if ( isUnmodifiedDefaultBlock( getBlock( end ) ) ) {
+ return getBlockIndex( end, destinationRootClientId );
+ }
+
+ // we insert after the selected block.
+ return getBlockIndex( end, destinationRootClientId ) + 1;
+ }
+
+ // Otherwise, we insert at the end of the current rootClientId
+ return getBlockOrder( destinationRootClientId ).length;
+ }
+
+ return {
+ showInsertionPoint() {
+ const index = getInsertionIndex();
+ showInsertionPoint( ownProps.destinationRootClientId, index );
+ },
+ hideInsertionPoint,
+ onSelect( item ) {
+ const {
+ replaceBlocks,
+ insertBlock,
+ } = dispatch( 'core/block-editor' );
+ const {
+ getSelectedBlock,
+ } = select( 'core/block-editor' );
+ const { isAppender } = ownProps;
+ const { name, initialAttributes } = item;
+ const selectedBlock = getSelectedBlock();
+ const insertedBlock = createBlock( name, initialAttributes );
+ if ( ! isAppender && selectedBlock && isUnmodifiedDefaultBlock( selectedBlock ) ) {
+ replaceBlocks( selectedBlock.clientId, insertedBlock );
+ } else {
+ insertBlock(
+ insertedBlock,
+ getInsertionIndex(),
+ ownProps.destinationRootClientId
+ );
+ }
+
+ ownProps.onSelect();
+ },
+ };
+ } ),
+ withInstanceId,
+)( InserterMenu );
diff --git a/packages/block-editor/src/components/inserter/style.native.scss b/packages/block-editor/src/components/inserter/style.native.scss
index 82d5fa58226504..e10b685dda406c 100644
--- a/packages/block-editor/src/components/inserter/style.native.scss
+++ b/packages/block-editor/src/components/inserter/style.native.scss
@@ -55,3 +55,10 @@
font-size: 12;
color: $gray-dark;
}
+
+.addBlockButton {
+ color: $blue-wordpress;
+ border: 2px;
+ border-radius: 10px;
+ border-color: $blue-wordpress;
+}
diff --git a/packages/components/src/dropdown/index.native.js b/packages/components/src/dropdown/index.native.js
new file mode 100644
index 00000000000000..de7c7b7839613a
--- /dev/null
+++ b/packages/components/src/dropdown/index.native.js
@@ -0,0 +1,62 @@
+/**
+ * WordPress dependencies
+ */
+import { Component } from '@wordpress/element';
+
+class Dropdown extends Component {
+ constructor() {
+ super( ...arguments );
+
+ this.toggle = this.toggle.bind( this );
+ this.close = this.close.bind( this );
+
+ this.state = {
+ isOpen: false,
+ };
+ }
+
+ componentWillUnmount() {
+ const { isOpen } = this.state;
+ const { onToggle } = this.props;
+ if ( isOpen && onToggle ) {
+ onToggle( false );
+ }
+ }
+
+ componentDidUpdate( prevProps, prevState ) {
+ const { isOpen } = this.state;
+ const { onToggle } = this.props;
+ if ( prevState.isOpen !== isOpen && onToggle ) {
+ onToggle( isOpen );
+ }
+ }
+
+ toggle() {
+ this.setState( ( state ) => ( {
+ isOpen: ! state.isOpen,
+ } ) );
+ }
+
+ close() {
+ this.setState( { isOpen: false } );
+ }
+
+ render() {
+ const { isOpen } = this.state;
+ const {
+ renderContent,
+ renderToggle,
+ } = this.props;
+
+ const args = { isOpen, onToggle: this.toggle, onClose: this.close };
+
+ return (
+ <>
+ { renderToggle( args ) }
+ { isOpen && renderContent( args ) }
+ >
+ );
+ }
+}
+
+export default Dropdown;
diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js
index 5965c13e26eaeb..4984cdde2fd36d 100644
--- a/packages/components/src/index.native.js
+++ b/packages/components/src/index.native.js
@@ -1,6 +1,7 @@
// Components
export * from './primitives';
export { default as Dashicon } from './dashicon';
+export { default as Dropdown } from './dropdown';
export { default as Toolbar } from './toolbar';
export { default as ToolbarButton } from './toolbar-button';
export { default as Icon } from './icon';
diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js
index 341737b41bef2b..fb9dab39a03f2b 100644
--- a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js
+++ b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js
@@ -5,8 +5,7 @@ import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view
import { FlatList } from 'react-native';
export const KeyboardAwareFlatList = ( {
- blockToolbarHeight,
- innerToolbarHeight,
+ extraScrollHeight,
shouldPreventAutomaticScroll,
innerRef,
...listProps
@@ -16,9 +15,7 @@ export const KeyboardAwareFlatList = ( {
keyboardDismissMode="none"
enableResetScrollToCoords={ false }
keyboardShouldPersistTaps="handled"
- extraScrollHeight={ innerToolbarHeight }
- extraBottomInset={ -listProps.safeAreaBottomInset }
- inputAccessoryViewHeight={ blockToolbarHeight }
+ extraScrollHeight={ extraScrollHeight }
extraHeight={ 0 }
innerRef={ ( ref ) => {
this.scrollViewRef = ref;
diff --git a/packages/edit-post/src/components/header/header-toolbar/index.native.js b/packages/edit-post/src/components/header/header-toolbar/index.native.js
new file mode 100644
index 00000000000000..9c71845889d371
--- /dev/null
+++ b/packages/edit-post/src/components/header/header-toolbar/index.native.js
@@ -0,0 +1,102 @@
+/**
+ * External dependencies
+ */
+import { ScrollView, Keyboard, Platform, View } from 'react-native';
+
+/**
+ * WordPress dependencies
+ */
+import { compose } from '@wordpress/compose';
+import { withSelect, withDispatch } from '@wordpress/data';
+import { withViewportMatch } from '@wordpress/viewport';
+import { __ } from '@wordpress/i18n';
+import {
+ Inserter,
+ BlockToolbar,
+} from '@wordpress/block-editor';
+import { Toolbar, ToolbarButton } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import styles from './style.scss';
+
+function HeaderToolbar( {
+ hasFixedToolbar,
+ hasRedo,
+ hasUndo,
+ redo,
+ undo,
+ showInserter,
+ showKeyboardHideButton,
+ clearSelectedBlock,
+} ) {
+ const hideKeyboard = () => {
+ clearSelectedBlock();
+ if ( Platform.OS === 'android' ) {
+ // Avoiding extra blur calls on iOS but still needed for android.
+ Keyboard.dismiss();
+ }
+ };
+
+ return (
+
+
+
+
+ { /* TODO: replace with EditorHistoryRedo and EditorHistoryUndo */ }
+
+
+
+ { hasFixedToolbar &&
+
+ }
+
+ { showKeyboardHideButton &&
+
+
+
+ }
+
+ );
+}
+
+export default compose( [
+ withSelect( ( select ) => ( {
+ hasRedo: select( 'core/editor' ).hasEditorRedo(),
+ hasUndo: select( 'core/editor' ).hasEditorUndo(),
+ hasFixedToolbar: select( 'core/edit-post' ).isFeatureActive( 'fixedToolbar' ),
+ // This setting (richEditingEnabled) should not live in the block editor's setting.
+ showInserter: select( 'core/edit-post' ).getEditorMode() === 'visual' && select( 'core/editor' ).getEditorSettings().richEditingEnabled,
+ isTextModeEnabled: select( 'core/edit-post' ).getEditorMode() === 'text',
+ } ) ),
+ withDispatch( ( dispatch ) => ( {
+ redo: dispatch( 'core/editor' ).redo,
+ undo: dispatch( 'core/editor' ).undo,
+ clearSelectedBlock: dispatch( 'core/editor' ).clearSelectedBlock,
+ } ) ),
+ withViewportMatch( { isLargeViewport: 'medium' } ),
+] )( HeaderToolbar );
diff --git a/packages/block-editor/src/components/block-toolbar/style.native.scss b/packages/edit-post/src/components/header/header-toolbar/style.native.scss
similarity index 75%
rename from packages/block-editor/src/components/block-toolbar/style.native.scss
rename to packages/edit-post/src/components/header/header-toolbar/style.native.scss
index 43e996cd8e0755..0aa03b90ed81cc 100644
--- a/packages/block-editor/src/components/block-toolbar/style.native.scss
+++ b/packages/edit-post/src/components/header/header-toolbar/style.native.scss
@@ -1,3 +1,4 @@
+
.container {
height: 44px;
flex-direction: row;
@@ -18,10 +19,3 @@
justify-content: center;
align-items: center;
}
-
-.addBlockButton {
- color: $blue-wordpress;
- border: 2px;
- border-radius: 10px;
- border-color: $blue-wordpress;
-}
diff --git a/packages/edit-post/src/components/header/index.native.js b/packages/edit-post/src/components/header/index.native.js
new file mode 100644
index 00000000000000..93fbf88ce79674
--- /dev/null
+++ b/packages/edit-post/src/components/header/index.native.js
@@ -0,0 +1,51 @@
+/**
+ * External dependencies
+ */
+import { Keyboard } from 'react-native';
+
+/**
+ * WordPress dependencies
+ */
+import { Component } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import HeaderToolbar from './header-toolbar';
+
+export default class Header extends Component {
+ constructor() {
+ super( ...arguments );
+
+ this.keyboardDidShow = this.keyboardDidShow.bind( this );
+ this.keyboardDidHide = this.keyboardDidHide.bind( this );
+
+ this.state = {
+ isKeyboardVisible: false,
+ };
+ }
+
+ componentDidMount() {
+ Keyboard.addListener( 'keyboardDidShow', this.keyboardDidShow );
+ Keyboard.addListener( 'keyboardDidHide', this.keyboardDidHide );
+ }
+
+ componentWillUnmount() {
+ Keyboard.removeListener( 'keyboardDidShow', this.keyboardDidShow );
+ Keyboard.removeListener( 'keyboardDidHide', this.keyboardDidHide );
+ }
+
+ keyboardDidShow() {
+ this.setState( { isKeyboardVisible: true } );
+ }
+
+ keyboardDidHide() {
+ this.setState( { isKeyboardVisible: false } );
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
diff --git a/packages/edit-post/src/components/layout/index.native.js b/packages/edit-post/src/components/layout/index.native.js
index 6081dc73410609..84f7e5bcde21dd 100644
--- a/packages/edit-post/src/components/layout/index.native.js
+++ b/packages/edit-post/src/components/layout/index.native.js
@@ -1,8 +1,9 @@
/**
* External dependencies
*/
-import { SafeAreaView } from 'react-native';
+import { Platform, SafeAreaView, View } from 'react-native';
import SafeArea from 'react-native-safe-area';
+import { sendNativeEditorDidLayout } from 'react-native-gutenberg-bridge';
/**
* WordPress dependencies
@@ -10,12 +11,14 @@ import SafeArea from 'react-native-safe-area';
import { Component } from '@wordpress/element';
import { withSelect } from '@wordpress/data';
import { compose } from '@wordpress/compose';
-import { HTMLTextInput, ReadableContentView } from '@wordpress/components';
+import { HTMLTextInput, KeyboardAvoidingView, ReadableContentView } from '@wordpress/components';
/**
* Internal dependencies
*/
import styles from './style.scss';
+import headerToolbarStyles from '../header/header-toolbar/style.scss';
+import Header from '../header';
import VisualEditor from '../visual-editor';
class Layout extends Component {
@@ -27,7 +30,7 @@ class Layout extends Component {
this.state = {
rootViewHeight: 0,
- safeAreaBottomInset: 0,
+ safeAreaInsets: { top: 0, bottom: 0, right: 0, left: 0 },
isFullyBordered: true,
};
@@ -46,8 +49,8 @@ class Layout extends Component {
onSafeAreaInsetsUpdate( result ) {
const { safeAreaInsets } = result;
- if ( this._isMounted && this.state.safeAreaBottomInset !== safeAreaInsets.bottom ) {
- this.setState( { safeAreaBottomInset: safeAreaInsets.bottom } );
+ if ( this._isMounted ) {
+ this.setState( { safeAreaInsets } );
}
}
@@ -60,7 +63,7 @@ class Layout extends Component {
setHeightState( event ) {
const { height } = event.nativeEvent.layout;
- this.setState( { rootViewHeight: height }, this.props.onNativeEditorDidLayout );
+ this.setState( { rootViewHeight: height }, sendNativeEditorDidLayout );
}
setBorderStyleState() {
@@ -72,9 +75,7 @@ class Layout extends Component {
renderHTML() {
return (
-
+
);
}
@@ -90,8 +91,6 @@ class Layout extends Component {
return (
);
@@ -102,9 +101,28 @@ class Layout extends Component {
mode,
} = this.props;
+ // add a margin view at the bottom for the header
+ const marginBottom = Platform.OS === 'android' ? headerToolbarStyles.container.height : 0;
+
+ const toolbarKeyboardAvoidingViewStyle = {
+ ...styles.toolbarKeyboardAvoidingView,
+ left: this.state.safeAreaInsets.left,
+ right: this.state.safeAreaInsets.right,
+ };
+
return (
- { mode === 'text' ? this.renderHTML() : this.renderVisual() }
+
+ { mode === 'text' ? this.renderHTML() : this.renderVisual() }
+
+
+
+
+
+
);
}
diff --git a/packages/edit-post/src/components/layout/style.native.scss b/packages/edit-post/src/components/layout/style.native.scss
index badc66b9b619fb..e6d7e241bcd0df 100644
--- a/packages/edit-post/src/components/layout/style.native.scss
+++ b/packages/edit-post/src/components/layout/style.native.scss
@@ -4,3 +4,10 @@
justify-content: flex-start;
background-color: #fff;
}
+
+.toolbarKeyboardAvoidingView {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ left: 0;
+}
diff --git a/packages/edit-post/src/components/visual-editor/index.native.js b/packages/edit-post/src/components/visual-editor/index.native.js
index 092ea638f68026..15ca4ed9d451bc 100644
--- a/packages/edit-post/src/components/visual-editor/index.native.js
+++ b/packages/edit-post/src/components/visual-editor/index.native.js
@@ -5,7 +5,7 @@ import { Component } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { withDispatch, withSelect } from '@wordpress/data';
import { compose } from '@wordpress/compose';
-import { BlockEditorProvider, BlockList } from '@wordpress/block-editor';
+import { BlockList } from '@wordpress/block-editor';
import { PostTitle } from '@wordpress/editor';
import { ReadableContentView } from '@wordpress/components';
@@ -43,30 +43,16 @@ class VisualEditor extends Component {
render() {
const {
- blocks,
isFullyBordered,
- resetEditorBlocks,
- resetEditorBlocksWithoutUndoLevel,
- rootViewHeight,
safeAreaBottomInset,
} = this.props;
return (
-
-
-
+
);
}
}
@@ -74,21 +60,16 @@ class VisualEditor extends Component {
export default compose( [
withSelect( ( select ) => {
const {
- getEditorBlocks,
getEditedPostAttribute,
- isPostTitleSelected,
} = select( 'core/editor' );
return {
- blocks: getEditorBlocks(),
title: getEditedPostAttribute( 'title' ),
- isPostTitleSelected: isPostTitleSelected(),
};
} ),
withDispatch( ( dispatch ) => {
const {
editPost,
- resetEditorBlocks,
} = dispatch( 'core/editor' );
const { clearSelectedBlock } = dispatch( 'core/block-editor' );
@@ -98,12 +79,6 @@ export default compose( [
editTitle( title ) {
editPost( { title } );
},
- resetEditorBlocks,
- resetEditorBlocksWithoutUndoLevel( blocks ) {
- resetEditorBlocks( blocks, {
- __unstableShouldCreateUndoLevel: false,
- } );
- },
};
} ),
] )( VisualEditor );
diff --git a/packages/edit-post/src/editor.native.js b/packages/edit-post/src/editor.native.js
index 71f8bbd9cb7746..23e0bad6a9e60a 100644
--- a/packages/edit-post/src/editor.native.js
+++ b/packages/edit-post/src/editor.native.js
@@ -1,22 +1,18 @@
/**
* External dependencies
*/
-import RNReactNativeGutenbergBridge, {
- subscribeParentGetHtml,
- subscribeParentToggleHTMLMode,
- subscribeUpdateHtml,
- subscribeSetFocusOnTitle,
- subscribeSetTitle,
- sendNativeEditorDidLayout,
-} from 'react-native-gutenberg-bridge';
+import memize from 'memize';
+import { size, map, without } from 'lodash';
/**
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
-import { parse, serialize, getUnregisteredTypeHandlerName } from '@wordpress/blocks';
+import { EditorProvider } from '@wordpress/editor';
+import { parse, serialize } from '@wordpress/blocks';
import { withDispatch, withSelect } from '@wordpress/data';
import { compose } from '@wordpress/compose';
+import { SlotFillProvider } from '@wordpress/components';
/**
* Internal dependencies
@@ -27,175 +23,119 @@ class Editor extends Component {
constructor( props ) {
super( ...arguments );
- this.setTitleRef = this.setTitleRef.bind( this );
-
- // TODO: use EditorProvider instead
- this.post = props.post || {
- id: 1,
- title: {
- raw: props.initialTitle,
- },
- content: {
- raw: props.initialHtml || '',
- },
- type: 'draft',
- };
-
- props.setupEditor( this.post );
-
- // make sure the post content is in sync with gutenberg store
- // to avoid marking the post as modified when simply loaded
- // For now, let's assume: serialize( parse( html ) ) !== html
- this.post.content.raw = serialize( props.getEditorBlocks() );
-
if ( props.initialHtmlModeEnabled && props.mode === 'visual' ) {
// enable html mode if the initial mode the parent wants it but we're not already in it
- this.toggleMode();
+ this.props.switchEditorMode( 'text' );
}
- }
-
- componentDidMount() {
- this.subscriptionParentGetHtml = subscribeParentGetHtml( () => {
- this.serializeToNativeAction();
- } );
- this.subscriptionParentToggleHTMLMode = subscribeParentToggleHTMLMode( () => {
- this.toggleMode();
+ this.getEditorSettings = memize( this.getEditorSettings, {
+ maxSize: 1,
} );
-
- this.subscriptionParentSetTitle = subscribeSetTitle( ( payload ) => {
- this.props.editTitle( payload.title );
- } );
-
- this.subscriptionParentUpdateHtml = subscribeUpdateHtml( ( payload ) => {
- this.updateHtmlAction( payload.html );
- } );
-
- this.subscriptionParentSetFocusOnTitle = subscribeSetFocusOnTitle( () => {
- if ( this.postTitleRef ) {
- this.postTitleRef.focus();
- }
- } );
- }
-
- componentWillUnmount() {
- if ( this.subscriptionParentGetHtml ) {
- this.subscriptionParentGetHtml.remove();
- }
-
- if ( this.subscriptionParentToggleHTMLMode ) {
- this.subscriptionParentToggleHTMLMode.remove();
- }
-
- if ( this.subscriptionParentSetTitle ) {
- this.subscriptionParentSetTitle.remove();
- }
-
- if ( this.subscriptionParentUpdateHtml ) {
- this.subscriptionParentUpdateHtml.remove();
- }
-
- if ( this.subscriptionParentSetFocusOnTitle ) {
- this.subscriptionParentSetFocusOnTitle.remove();
- }
}
- serializeToNativeAction() {
- if ( this.props.mode === 'text' ) {
- this.updateHtmlAction( this.props.getEditedPostContent() );
- }
-
- const html = serialize( this.props.getEditorBlocks() );
- const title = this.props.getEditedPostAttribute( 'title' );
-
- const hasChanges = title !== this.post.title.raw || html !== this.post.content.raw;
-
- RNReactNativeGutenbergBridge.provideToNative_Html( html, title, hasChanges );
+ getEditorSettings(
+ settings,
+ hasFixedToolbar,
+ focusMode,
+ hiddenBlockTypes,
+ blockTypes,
+ ) {
+ settings = {
+ ...settings,
+ hasFixedToolbar,
+ focusMode,
+ };
- if ( hasChanges ) {
- this.post.title.raw = title;
- this.post.content.raw = html;
+ // Omit hidden block types if exists and non-empty.
+ if ( size( hiddenBlockTypes ) > 0 ) {
+ // Defer to passed setting for `allowedBlockTypes` if provided as
+ // anything other than `true` (where `true` is equivalent to allow
+ // all block types).
+ const defaultAllowedBlockTypes = (
+ true === settings.allowedBlockTypes ?
+ map( blockTypes, 'name' ) :
+ ( settings.allowedBlockTypes || [] )
+ );
+
+ settings.allowedBlockTypes = without(
+ defaultAllowedBlockTypes,
+ ...hiddenBlockTypes,
+ );
}
- }
-
- updateHtmlAction( html ) {
- const parsed = parse( html );
- this.props.resetEditorBlocksWithoutUndoLevel( parsed );
- }
- toggleMode() {
- const { mode, switchMode } = this.props;
- // refresh html content first
- this.serializeToNativeAction();
- switchMode( mode === 'visual' ? 'text' : 'visual' );
+ return settings;
}
- componentDidUpdate( prevProps ) {
- if ( ! prevProps.isReady && this.props.isReady ) {
- const blocks = this.props.getEditorBlocks();
- const isUnsupportedBlock = ( { name } ) => name === getUnregisteredTypeHandlerName();
- const unsupportedBlockNames = blocks.filter( isUnsupportedBlock ).map( ( block ) => block.attributes.originalName );
- RNReactNativeGutenbergBridge.editorDidMount( unsupportedBlockNames );
- }
- }
+ render() {
+ const {
+ settings,
+ hasFixedToolbar,
+ focusMode,
+ initialEdits,
+ hiddenBlockTypes,
+ blockTypes,
+ post,
+ ...props
+ } = this.props;
+
+ const editorSettings = this.getEditorSettings(
+ settings,
+ hasFixedToolbar,
+ focusMode,
+ hiddenBlockTypes,
+ blockTypes,
+ );
- setTitleRef( titleRef ) {
- this.postTitleRef = titleRef;
- }
+ const normalizedPost = post || {
+ id: 1,
+ title: {
+ raw: props.initialTitle,
+ },
+ content: {
+ // make sure the post content is in sync with gutenberg store
+ // to avoid marking the post as modified when simply loaded
+ // For now, let's assume: serialize( parse( html ) ) !== html
+ raw: serialize( parse( props.initialHtml || '' ) ),
+ },
+ type: 'draft',
+ };
- render() {
return (
-
+
+
+
+
+
);
}
}
export default compose( [
withSelect( ( select ) => {
- const {
- __unstableIsEditorReady: isEditorReady,
- getEditorBlocks,
- getEditedPostAttribute,
- getEditedPostContent,
- } = select( 'core/editor' );
- const {
- getEditorMode,
- } = select( 'core/edit-post' );
+ const { isFeatureActive, getEditorMode, getPreference } = select( 'core/edit-post' );
+ const { getBlockTypes } = select( 'core/blocks' );
return {
+ hasFixedToolbar: isFeatureActive( 'fixedToolbar' ),
+ focusMode: isFeatureActive( 'focusMode' ),
mode: getEditorMode(),
- isReady: isEditorReady(),
- getEditorBlocks,
- getEditedPostAttribute,
- getEditedPostContent,
+ hiddenBlockTypes: getPreference( 'hiddenBlockTypes' ),
+ blockTypes: getBlockTypes(),
};
} ),
withDispatch( ( dispatch ) => {
- const {
- editPost,
- setupEditor,
- resetEditorBlocks,
- } = dispatch( 'core/editor' );
const {
switchEditorMode,
} = dispatch( 'core/edit-post' );
return {
- editTitle( title ) {
- editPost( { title } );
- },
- resetEditorBlocksWithoutUndoLevel( blocks ) {
- resetEditorBlocks( blocks, {
- __unstableShouldCreateUndoLevel: false,
- } );
- },
- setupEditor,
- switchMode( mode ) {
- switchEditorMode( mode );
- },
+ switchEditorMode,
};
} ),
] )( Editor );
diff --git a/packages/edit-post/src/index.native.js b/packages/edit-post/src/index.native.js
index 1ffdcb13477335..296e37117e5a1d 100644
--- a/packages/edit-post/src/index.native.js
+++ b/packages/edit-post/src/index.native.js
@@ -4,6 +4,7 @@
import '@wordpress/core-data';
import '@wordpress/block-editor';
import '@wordpress/editor';
+import '@wordpress/viewport';
import '@wordpress/notices';
import { registerCoreBlocks } from '@wordpress/block-library';
import { unregisterBlockType } from '@wordpress/blocks';
diff --git a/packages/edit-post/src/store/defaults.native.js b/packages/edit-post/src/store/defaults.native.js
new file mode 100644
index 00000000000000..7ac420b872cadc
--- /dev/null
+++ b/packages/edit-post/src/store/defaults.native.js
@@ -0,0 +1,14 @@
+export const PREFERENCES_DEFAULTS = {
+ editorMode: 'visual',
+ isGeneralSidebarDismissed: true,
+ panels: {
+ 'post-status': {
+ opened: true,
+ },
+ },
+ features: {
+ fixedToolbar: true,
+ },
+ pinnedPluginItems: {},
+ hiddenBlockTypes: [],
+};
diff --git a/packages/edit-post/src/test/editor.native.js b/packages/edit-post/src/test/editor.native.js
index aee6c0fe505860..cedd6bf8c94b37 100644
--- a/packages/edit-post/src/test/editor.native.js
+++ b/packages/edit-post/src/test/editor.native.js
@@ -15,7 +15,7 @@ jest.mock( '../components/layout', () => () => 'Layout' );
/**
* Internal dependencies
*/
-import '../store';
+import '..';
import Editor from '../editor';
const unsupportedBlock = `
diff --git a/packages/editor/src/components/convert-to-group-buttons/index.native.js b/packages/editor/src/components/convert-to-group-buttons/index.native.js
new file mode 100644
index 00000000000000..bd0c2f440d06f2
--- /dev/null
+++ b/packages/editor/src/components/convert-to-group-buttons/index.native.js
@@ -0,0 +1,2 @@
+
+export default () => null;
diff --git a/packages/editor/src/components/index.native.js b/packages/editor/src/components/index.native.js
index 85a33d69bf2515..69035455d49f13 100644
--- a/packages/editor/src/components/index.native.js
+++ b/packages/editor/src/components/index.native.js
@@ -4,4 +4,7 @@ export { default as PostTitle } from './post-title';
export { default as EditorHistoryRedo } from './editor-history/redo';
export { default as EditorHistoryUndo } from './editor-history/undo';
+// State Related Components
+export { default as EditorProvider } from './provider';
+
export * from './deprecated';
diff --git a/packages/editor/src/components/provider/index.native.js b/packages/editor/src/components/provider/index.native.js
new file mode 100644
index 00000000000000..d2b1458d3e2e56
--- /dev/null
+++ b/packages/editor/src/components/provider/index.native.js
@@ -0,0 +1,182 @@
+/**
+ * External dependencies
+ */
+import RNReactNativeGutenbergBridge, {
+ subscribeParentGetHtml,
+ subscribeParentToggleHTMLMode,
+ subscribeUpdateHtml,
+ subscribeSetFocusOnTitle,
+ subscribeSetTitle,
+} from 'react-native-gutenberg-bridge';
+
+/**
+ * WordPress dependencies
+ */
+import { Component } from '@wordpress/element';
+import { parse, serialize, getUnregisteredTypeHandlerName } from '@wordpress/blocks';
+import { withDispatch, withSelect } from '@wordpress/data';
+import { compose } from '@wordpress/compose';
+
+/**
+ * Internal dependencies
+ */
+import EditorProvider from './index.js';
+
+class NativeEditorProvider extends Component {
+ constructor( props ) {
+ super( ...arguments );
+
+ // Keep a local reference to `post` to detect changes
+ this.post = props.post;
+
+ this.setTitleRef = this.setTitleRef.bind( this );
+ }
+
+ componentDidMount() {
+ this.subscriptionParentGetHtml = subscribeParentGetHtml( () => {
+ this.serializeToNativeAction();
+ } );
+
+ this.subscriptionParentToggleHTMLMode = subscribeParentToggleHTMLMode( () => {
+ this.toggleMode();
+ } );
+
+ this.subscriptionParentSetTitle = subscribeSetTitle( ( payload ) => {
+ this.props.editTitle( payload.title );
+ } );
+
+ this.subscriptionParentUpdateHtml = subscribeUpdateHtml( ( payload ) => {
+ this.updateHtmlAction( payload.html );
+ } );
+
+ this.subscriptionParentSetFocusOnTitle = subscribeSetFocusOnTitle( () => {
+ if ( this.postTitleRef ) {
+ this.postTitleRef.focus();
+ }
+ } );
+ }
+
+ componentWillUnmount() {
+ if ( this.subscriptionParentGetHtml ) {
+ this.subscriptionParentGetHtml.remove();
+ }
+
+ if ( this.subscriptionParentToggleHTMLMode ) {
+ this.subscriptionParentToggleHTMLMode.remove();
+ }
+
+ if ( this.subscriptionParentSetTitle ) {
+ this.subscriptionParentSetTitle.remove();
+ }
+
+ if ( this.subscriptionParentUpdateHtml ) {
+ this.subscriptionParentUpdateHtml.remove();
+ }
+
+ if ( this.subscriptionParentSetFocusOnTitle ) {
+ this.subscriptionParentSetFocusOnTitle.remove();
+ }
+ }
+
+ componentDidUpdate( prevProps ) {
+ if ( ! prevProps.isReady && this.props.isReady ) {
+ const blocks = this.props.blocks;
+ const isUnsupportedBlock = ( { name } ) => name === getUnregisteredTypeHandlerName();
+ const unsupportedBlockNames = blocks.filter( isUnsupportedBlock ).map( ( block ) => block.attributes.originalName );
+ RNReactNativeGutenbergBridge.editorDidMount( unsupportedBlockNames );
+ }
+ }
+
+ setTitleRef( titleRef ) {
+ this.postTitleRef = titleRef;
+ }
+
+ serializeToNativeAction() {
+ if ( this.props.mode === 'text' ) {
+ this.updateHtmlAction( this.props.getEditedPostContent() );
+ }
+
+ const html = serialize( this.props.blocks );
+ const title = this.props.title;
+
+ const hasChanges = title !== this.post.title.raw || html !== this.post.content.raw;
+
+ RNReactNativeGutenbergBridge.provideToNative_Html( html, title, hasChanges );
+
+ if ( hasChanges ) {
+ this.post.title.raw = title;
+ this.post.content.raw = html;
+ }
+ }
+
+ updateHtmlAction( html ) {
+ const parsed = parse( html );
+ this.props.resetEditorBlocksWithoutUndoLevel( parsed );
+ }
+
+ toggleMode() {
+ const { mode, switchMode } = this.props;
+ // refresh html content first
+ this.serializeToNativeAction();
+ switchMode( mode === 'visual' ? 'text' : 'visual' );
+ }
+
+ render() {
+ const {
+ children,
+ post, // eslint-disable-line no-unused-vars
+ ...props
+ } = this.props;
+
+ return (
+
+ { children }
+
+ );
+ }
+}
+
+export default compose( [
+ withSelect( ( select ) => {
+ const {
+ __unstableIsEditorReady: isEditorReady,
+ getEditorBlocks,
+ getEditedPostAttribute,
+ getEditedPostContent,
+ } = select( 'core/editor' );
+ const {
+ getEditorMode,
+ } = select( 'core/edit-post' );
+
+ return {
+ mode: getEditorMode(),
+ isReady: isEditorReady(),
+ blocks: getEditorBlocks(),
+ title: getEditedPostAttribute( 'title' ),
+ getEditedPostContent,
+ };
+ } ),
+ withDispatch( ( dispatch ) => {
+ const {
+ editPost,
+ resetEditorBlocks,
+ } = dispatch( 'core/editor' );
+ const {
+ switchEditorMode,
+ } = dispatch( 'core/edit-post' );
+
+ return {
+ editTitle( title ) {
+ editPost( { title } );
+ },
+ resetEditorBlocksWithoutUndoLevel( blocks ) {
+ resetEditorBlocks( blocks, {
+ __unstableShouldCreateUndoLevel: false,
+ } );
+ },
+ switchMode( mode ) {
+ switchEditorMode( mode );
+ },
+ };
+ } ),
+] )( NativeEditorProvider );
diff --git a/packages/editor/src/components/reusable-blocks-buttons/index.native.js b/packages/editor/src/components/reusable-blocks-buttons/index.native.js
new file mode 100644
index 00000000000000..bd0c2f440d06f2
--- /dev/null
+++ b/packages/editor/src/components/reusable-blocks-buttons/index.native.js
@@ -0,0 +1,2 @@
+
+export default () => null;
diff --git a/packages/viewport/src/index.native.js b/packages/viewport/src/index.native.js
index 8b137891791fe9..605b511ac9d686 100644
--- a/packages/viewport/src/index.native.js
+++ b/packages/viewport/src/index.native.js
@@ -1 +1,8 @@
+/**
+ * Internal dependencies
+ */
+import './store';
+
+export { default as ifViewportMatches } from './if-viewport-matches';
+export { default as withViewportMatch } from './with-viewport-match';