diff --git a/packages/block-editor/src/components/block-list/block-list-context.native.js b/packages/block-editor/src/components/block-list/block-list-context.native.js
new file mode 100644
index 00000000000000..95385b480b3d14
--- /dev/null
+++ b/packages/block-editor/src/components/block-list/block-list-context.native.js
@@ -0,0 +1,131 @@
+/**
+ * WordPress dependencies
+ */
+import { createContext, useContext } from '@wordpress/element';
+
+export const DEFAULT_BLOCK_LIST_CONTEXT = {
+ scrollRef: null,
+ blocksLayouts: { current: {} },
+ findBlockLayoutByClientId,
+ updateBlocksLayouts,
+};
+
+const Context = createContext( DEFAULT_BLOCK_LIST_CONTEXT );
+const { Provider, Consumer } = Context;
+
+/**
+ * Finds a block's layout data by its client Id.
+ *
+ * @param {Object} data Blocks layouts object.
+ * @param {string} clientId Block's clientId.
+ *
+ * @return {Object} Found block layout data.
+ */
+function findBlockLayoutByClientId( data, clientId ) {
+ return Object.entries( data ).reduce( ( acc, entry ) => {
+ const item = entry[ 1 ];
+ if ( acc ) {
+ return acc;
+ }
+ if ( item?.clientId === clientId ) {
+ return item;
+ }
+ if ( item?.innerBlocks && Object.keys( item.innerBlocks ).length > 0 ) {
+ return findBlockLayoutByClientId( item.innerBlocks, clientId );
+ }
+ return null;
+ }, null );
+}
+
+/**
+ * Deletes the layout data of a block by its client Id.
+ *
+ * @param {Object} data Blocks layouts object.
+ * @param {string} clientId Block's clientsId.
+ *
+ * @return {Object} Updated data object.
+ */
+export function deleteBlockLayoutByClientId( data, clientId ) {
+ return Object.keys( data ).reduce( ( acc, key ) => {
+ if ( key !== clientId ) {
+ acc[ key ] = data[ key ];
+ }
+ if (
+ data[ key ]?.innerBlocks &&
+ Object.keys( data[ key ].innerBlocks ).length > 0
+ ) {
+ if ( acc[ key ] ) {
+ acc[ key ].innerBlocks = deleteBlockLayoutByClientId(
+ data[ key ].innerBlocks,
+ clientId
+ );
+ }
+ }
+ return acc;
+ }, {} );
+}
+
+/**
+ * Updates or deletes a block's layout data in the blocksLayouts object,
+ * in case of deletion, the layout data is not required.
+ *
+ * @param {Object} blocksLayouts Blocks layouts object.
+ * @param {Object} blockData Block's layout data to add or remove to/from the blockLayouts object.
+ * @param {string} blockData.clientId Block's clientId.
+ * @param {?string} blockData.rootClientId Optional. Block's rootClientId.
+ * @param {?boolean} blockData.shouldRemove Optional. Flag to remove it from the blocksLayout list.
+ * @param {number} blockData.width Block's width.
+ * @param {number} blockData.height Block's height.
+ * @param {number} blockData.x Block's x coordinate (relative to the parent).
+ * @param {number} blockData.y Block's y coordinate (relative to the parent).
+ */
+
+function updateBlocksLayouts( blocksLayouts, blockData ) {
+ const { clientId, rootClientId, shouldRemove, ...layoutProps } = blockData;
+
+ if ( clientId && shouldRemove ) {
+ blocksLayouts.current = deleteBlockLayoutByClientId(
+ blocksLayouts.current,
+ clientId
+ );
+ return;
+ }
+
+ if ( clientId && ! rootClientId ) {
+ blocksLayouts.current[ clientId ] = {
+ clientId,
+ rootClientId,
+ ...layoutProps,
+ innerBlocks: {
+ ...blocksLayouts.current[ clientId ]?.innerBlocks,
+ },
+ };
+ } else if ( clientId && rootClientId ) {
+ const block = findBlockLayoutByClientId(
+ blocksLayouts.current,
+ rootClientId
+ );
+
+ if ( block ) {
+ block.innerBlocks[ clientId ] = {
+ clientId,
+ rootClientId,
+ ...layoutProps,
+ innerBlocks: {
+ ...block.innerBlocks[ clientId ]?.innerBlocks,
+ },
+ };
+ }
+ }
+}
+
+export { Provider as BlockListProvider, Consumer as BlockListConsumer };
+
+/**
+ * Hook that returns the block list context.
+ *
+ * @return {Object} Block list context
+ */
+export const useBlockListContext = () => {
+ return useContext( Context );
+};
diff --git a/packages/block-editor/src/components/block-list/block-list-item-cell.native.js b/packages/block-editor/src/components/block-list/block-list-item-cell.native.js
new file mode 100644
index 00000000000000..c399643a633996
--- /dev/null
+++ b/packages/block-editor/src/components/block-list/block-list-item-cell.native.js
@@ -0,0 +1,42 @@
+/**
+ * External dependencies
+ */
+import { View } from 'react-native';
+
+/**
+ * WordPress dependencies
+ */
+import { useEffect, useCallback } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { useBlockListContext } from './block-list-context';
+
+function BlockListItemCell( { children, clientId, rootClientId } ) {
+ const { blocksLayouts, updateBlocksLayouts } = useBlockListContext();
+
+ useEffect( () => {
+ return () => {
+ updateBlocksLayouts( blocksLayouts, {
+ clientId,
+ shouldRemove: true,
+ } );
+ };
+ }, [] );
+
+ const onLayout = useCallback(
+ ( { nativeEvent: { layout } } ) => {
+ updateBlocksLayouts( blocksLayouts, {
+ clientId,
+ rootClientId,
+ ...layout,
+ } );
+ },
+ [ clientId, rootClientId, updateBlocksLayouts ]
+ );
+
+ return { children };
+}
+
+export default BlockListItemCell;
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 ada27b4ebd9fbf..f1dc31da0e91eb 100644
--- a/packages/block-editor/src/components/block-list/index.native.js
+++ b/packages/block-editor/src/components/block-list/index.native.js
@@ -25,10 +25,14 @@ import { __ } from '@wordpress/i18n';
import styles from './style.scss';
import BlockListAppender from '../block-list-appender';
import BlockListItem from './block-list-item';
+import BlockListItemCell from './block-list-item-cell';
+import {
+ BlockListProvider,
+ BlockListConsumer,
+ DEFAULT_BLOCK_LIST_CONTEXT,
+} from './block-list-context';
import { store as blockEditorStore } from '../../store';
-const BlockListContext = createContext();
-
export const OnCaretVerticalPositionChange = createContext();
const stylesMemo = {};
@@ -78,6 +82,9 @@ export class BlockList extends Component {
);
this.renderEmptyList = this.renderEmptyList.bind( this );
this.getExtraData = this.getExtraData.bind( this );
+ this.getCellRendererComponent = this.getCellRendererComponent.bind(
+ this
+ );
this.onLayout = this.onLayout.bind( this );
@@ -154,6 +161,17 @@ export class BlockList extends Component {
return this.extraData;
}
+ getCellRendererComponent( { children, item } ) {
+ const { rootClientId } = this.props;
+ return (
+
+ );
+ }
+
onLayout( { nativeEvent } ) {
const { layout } = nativeEvent;
const { blockWidth } = this.state;
@@ -173,17 +191,22 @@ export class BlockList extends Component {
const { isRootList } = this.props;
// Use of Context to propagate the main scroll ref to its children e.g InnerBlocks.
const blockList = isRootList ? (
-
+
{ this.renderList() }
-
+
) : (
-
- { ( ref ) =>
+
+ { ( { scrollRef } ) =>
this.renderList( {
- parentScrollRef: ref,
+ parentScrollRef: scrollRef,
} )
}
-
+
);
return (
@@ -279,6 +302,7 @@ export class BlockList extends Component {
data={ blockClientIds }
keyExtractor={ identity }
renderItem={ this.renderItem }
+ CellRendererComponent={ this.getCellRendererComponent }
shouldPreventAutomaticScroll={
this.shouldFlatListPreventAutomaticScroll
}
diff --git a/packages/block-editor/src/components/block-list/test/block-list-context.native.js b/packages/block-editor/src/components/block-list/test/block-list-context.native.js
new file mode 100644
index 00000000000000..13fd0b1e42cb6b
--- /dev/null
+++ b/packages/block-editor/src/components/block-list/test/block-list-context.native.js
@@ -0,0 +1,253 @@
+/**
+ * External dependencies
+ */
+import { cloneDeep } from 'lodash';
+
+/**
+ * Internal dependencies
+ */
+import {
+ DEFAULT_BLOCK_LIST_CONTEXT,
+ deleteBlockLayoutByClientId,
+} from '../block-list-context.native';
+import {
+ BLOCKS_LAYOUTS_DATA,
+ DEEP_NESTED_ID,
+ GROUP_BLOCK_LAYOUT_DATA,
+ NESTED_WITH_INNER_BLOCKS_ID,
+ PARAGRAPH_BLOCK_LAYOUT_DATA,
+ ROOT_LEVEL_ID,
+} from './fixtures/block-list-context.native';
+
+describe( 'findBlockLayoutByClientId', () => {
+ it( "finds a block's layout data at root level", () => {
+ const { findBlockLayoutByClientId } = DEFAULT_BLOCK_LIST_CONTEXT;
+ const currentBlockLayouts = BLOCKS_LAYOUTS_DATA;
+
+ const blockRootLevel = findBlockLayoutByClientId(
+ currentBlockLayouts,
+ ROOT_LEVEL_ID
+ );
+
+ expect( blockRootLevel ).toEqual(
+ expect.objectContaining( { clientId: ROOT_LEVEL_ID } )
+ );
+ } );
+
+ it( "finds a nested block's layout data with inner blocks", () => {
+ const { findBlockLayoutByClientId } = DEFAULT_BLOCK_LIST_CONTEXT;
+ const currentBlockLayouts = BLOCKS_LAYOUTS_DATA;
+
+ const nestedBlock = findBlockLayoutByClientId(
+ currentBlockLayouts,
+ NESTED_WITH_INNER_BLOCKS_ID
+ );
+
+ expect( nestedBlock ).toEqual(
+ expect.objectContaining( { clientId: NESTED_WITH_INNER_BLOCKS_ID } )
+ );
+ } );
+
+ it( "finds a deep nested block's layout data", () => {
+ const { findBlockLayoutByClientId } = DEFAULT_BLOCK_LIST_CONTEXT;
+ const currentBlockLayouts = BLOCKS_LAYOUTS_DATA;
+
+ const deepNestedBlock = findBlockLayoutByClientId(
+ currentBlockLayouts,
+ DEEP_NESTED_ID
+ );
+
+ expect( deepNestedBlock ).toEqual(
+ expect.objectContaining( { clientId: DEEP_NESTED_ID } )
+ );
+ } );
+} );
+
+describe( 'deleteBlockLayoutByClientId', () => {
+ it( "deletes a block's layout data at root level", () => {
+ const { findBlockLayoutByClientId } = DEFAULT_BLOCK_LIST_CONTEXT;
+ const defaultBlockLayouts = cloneDeep( BLOCKS_LAYOUTS_DATA );
+ const currentBlockLayouts = deleteBlockLayoutByClientId(
+ defaultBlockLayouts,
+ ROOT_LEVEL_ID
+ );
+
+ const findDeletedBlock = findBlockLayoutByClientId(
+ currentBlockLayouts,
+ ROOT_LEVEL_ID
+ );
+
+ expect( findDeletedBlock ).toBeNull();
+ } );
+
+ it( "deletes a nested block's layout data with inner blocks", () => {
+ const { findBlockLayoutByClientId } = DEFAULT_BLOCK_LIST_CONTEXT;
+ const defaultBlockLayouts = cloneDeep( BLOCKS_LAYOUTS_DATA );
+ const currentBlockLayouts = deleteBlockLayoutByClientId(
+ defaultBlockLayouts,
+ NESTED_WITH_INNER_BLOCKS_ID
+ );
+
+ const findDeletedBlock = findBlockLayoutByClientId(
+ currentBlockLayouts,
+ NESTED_WITH_INNER_BLOCKS_ID
+ );
+
+ expect( findDeletedBlock ).toBeNull();
+ } );
+
+ it( "deletes a deep nested block's layout data", () => {
+ const { findBlockLayoutByClientId } = DEFAULT_BLOCK_LIST_CONTEXT;
+ const defaultBlockLayouts = cloneDeep( BLOCKS_LAYOUTS_DATA );
+ const currentBlockLayouts = deleteBlockLayoutByClientId(
+ defaultBlockLayouts,
+ DEEP_NESTED_ID
+ );
+
+ const findDeletedBlock = findBlockLayoutByClientId(
+ currentBlockLayouts,
+ DEEP_NESTED_ID
+ );
+
+ expect( findDeletedBlock ).toBeNull();
+ } );
+} );
+
+describe( 'updateBlocksLayouts', () => {
+ it( "adds a new block's layout data at root level with an empty object", () => {
+ const {
+ blocksLayouts,
+ findBlockLayoutByClientId,
+ updateBlocksLayouts,
+ } = DEFAULT_BLOCK_LIST_CONTEXT;
+ const currentBlockLayouts = cloneDeep( blocksLayouts );
+ const BLOCK_CLIENT_ID = PARAGRAPH_BLOCK_LAYOUT_DATA.clientId;
+
+ updateBlocksLayouts( currentBlockLayouts, PARAGRAPH_BLOCK_LAYOUT_DATA );
+
+ const findAddedBlock = findBlockLayoutByClientId(
+ currentBlockLayouts.current,
+ BLOCK_CLIENT_ID
+ );
+
+ expect( findAddedBlock ).toEqual(
+ expect.objectContaining( {
+ clientId: BLOCK_CLIENT_ID,
+ rootClientId: undefined,
+ } )
+ );
+ } );
+
+ it( "adds a new block's layout data at root level with inner blocks", () => {
+ const {
+ findBlockLayoutByClientId,
+ updateBlocksLayouts,
+ } = DEFAULT_BLOCK_LIST_CONTEXT;
+ const currentBlockLayouts = {
+ current: cloneDeep( BLOCKS_LAYOUTS_DATA ),
+ };
+ const PARENT_BLOCK_CLIENT_ID = GROUP_BLOCK_LAYOUT_DATA.clientId;
+
+ // Add parent block
+ updateBlocksLayouts( currentBlockLayouts, GROUP_BLOCK_LAYOUT_DATA );
+
+ const findAddedParentBlock = findBlockLayoutByClientId(
+ currentBlockLayouts.current,
+ PARENT_BLOCK_CLIENT_ID
+ );
+
+ expect( findAddedParentBlock ).toEqual(
+ expect.objectContaining( { clientId: PARENT_BLOCK_CLIENT_ID } )
+ );
+
+ // Add inner block to it's parent
+ updateBlocksLayouts( currentBlockLayouts, {
+ ...PARAGRAPH_BLOCK_LAYOUT_DATA,
+ rootClientId: PARENT_BLOCK_CLIENT_ID,
+ } );
+
+ const findAddedInnerBlock = findBlockLayoutByClientId(
+ currentBlockLayouts.current,
+ PARAGRAPH_BLOCK_LAYOUT_DATA.clientId
+ );
+
+ expect( findAddedInnerBlock ).toEqual(
+ expect.objectContaining( {
+ clientId: PARAGRAPH_BLOCK_LAYOUT_DATA.clientId,
+ rootClientId: PARENT_BLOCK_CLIENT_ID,
+ } )
+ );
+ } );
+
+ it( "adds a new block's layout data at deep level", () => {
+ const {
+ findBlockLayoutByClientId,
+ updateBlocksLayouts,
+ } = DEFAULT_BLOCK_LIST_CONTEXT;
+ const currentBlockLayouts = {
+ current: cloneDeep( BLOCKS_LAYOUTS_DATA ),
+ };
+
+ // Add block layout data to it's parents inner blocks
+ updateBlocksLayouts( currentBlockLayouts, {
+ ...PARAGRAPH_BLOCK_LAYOUT_DATA,
+ rootClientId: NESTED_WITH_INNER_BLOCKS_ID,
+ } );
+
+ const findAddedInnerBlock = findBlockLayoutByClientId(
+ currentBlockLayouts.current,
+ PARAGRAPH_BLOCK_LAYOUT_DATA.clientId
+ );
+
+ expect( findAddedInnerBlock ).toEqual(
+ expect.objectContaining( {
+ clientId: PARAGRAPH_BLOCK_LAYOUT_DATA.clientId,
+ rootClientId: NESTED_WITH_INNER_BLOCKS_ID,
+ } )
+ );
+ } );
+
+ it( "deletes a block's layout data at root level", () => {
+ const {
+ findBlockLayoutByClientId,
+ updateBlocksLayouts,
+ } = DEFAULT_BLOCK_LIST_CONTEXT;
+ const currentBlockLayouts = {
+ current: cloneDeep( BLOCKS_LAYOUTS_DATA ),
+ };
+
+ updateBlocksLayouts( currentBlockLayouts, {
+ shouldRemove: true,
+ clientId: ROOT_LEVEL_ID,
+ } );
+
+ const findDeletedBlock = findBlockLayoutByClientId(
+ currentBlockLayouts.current,
+ ROOT_LEVEL_ID
+ );
+
+ expect( findDeletedBlock ).toBeNull();
+ } );
+
+ it( "deletes a block's layout data at a deep level", () => {
+ const {
+ findBlockLayoutByClientId,
+ updateBlocksLayouts,
+ } = DEFAULT_BLOCK_LIST_CONTEXT;
+ const currentBlockLayouts = {
+ current: cloneDeep( BLOCKS_LAYOUTS_DATA ),
+ };
+
+ updateBlocksLayouts( currentBlockLayouts, {
+ shouldRemove: true,
+ clientId: DEEP_NESTED_ID,
+ } );
+
+ const findDeletedBlock = findBlockLayoutByClientId(
+ currentBlockLayouts.current,
+ DEEP_NESTED_ID
+ );
+
+ expect( findDeletedBlock ).toBeNull();
+ } );
+} );
diff --git a/packages/block-editor/src/components/block-list/test/fixtures/block-list-context.native.js b/packages/block-editor/src/components/block-list/test/fixtures/block-list-context.native.js
new file mode 100644
index 00000000000000..af74c07ec8e0ae
--- /dev/null
+++ b/packages/block-editor/src/components/block-list/test/fixtures/block-list-context.native.js
@@ -0,0 +1,79 @@
+export const ROOT_LEVEL_ID = 'e59528f8-fb35-4ec1-aec6-5a065c236fa1';
+export const ROOT_LEVEL_WITH_INNER_BLOCKS_ID =
+ '72a9220f-4c3d-4b00-bae1-4506513f63d8';
+export const NESTED_WITH_INNER_BLOCKS_ID =
+ '9f3d1f1e-df85-485d-af63-dc8cb1b93cbc';
+export const DEEP_NESTED_ID = 'abec845a-e4de-43fb-96f7-80dc3d51ad7a';
+
+export const BLOCKS_LAYOUTS_DATA = {
+ [ ROOT_LEVEL_ID ]: {
+ clientId: ROOT_LEVEL_ID,
+ width: 390,
+ height: 54,
+ x: 0,
+ y: 83,
+ innerBlocks: {},
+ },
+ [ ROOT_LEVEL_WITH_INNER_BLOCKS_ID ]: {
+ clientId: ROOT_LEVEL_WITH_INNER_BLOCKS_ID,
+ width: 390,
+ height: 386,
+ x: 0,
+ y: 137,
+ innerBlocks: {
+ '62839858-48b0-44ed-b834-1343a1357e54': {
+ clientId: '62839858-48b0-44ed-b834-1343a1357e54',
+ rootClientId: ROOT_LEVEL_WITH_INNER_BLOCKS_ID,
+ width: 390,
+ height: 54,
+ x: 0,
+ y: 0,
+ innerBlocks: {},
+ },
+ [ NESTED_WITH_INNER_BLOCKS_ID ]: {
+ clientId: NESTED_WITH_INNER_BLOCKS_ID,
+ rootClientId: ROOT_LEVEL_WITH_INNER_BLOCKS_ID,
+ width: 390,
+ height: 332,
+ x: 0,
+ y: 54,
+ innerBlocks: {
+ '435d62a4-afa7-457c-a894-b04390d7b447': {
+ clientId: '435d62a4-afa7-457c-a894-b04390d7b447',
+ rootClientId: NESTED_WITH_INNER_BLOCKS_ID,
+ width: 358,
+ height: 54,
+ x: 0,
+ y: 0,
+ innerBlocks: {},
+ },
+ [ DEEP_NESTED_ID ]: {
+ clientId: DEEP_NESTED_ID,
+ rootClientId: NESTED_WITH_INNER_BLOCKS_ID,
+ width: 358,
+ height: 98,
+ x: 0,
+ y: 54,
+ innerBlocks: {},
+ },
+ },
+ },
+ },
+ },
+};
+
+export const PARAGRAPH_BLOCK_LAYOUT_DATA = {
+ clientId: '22dda04f-4718-45b2-8cd2-36cedb9eae4d',
+ width: 390,
+ height: 98,
+ x: 0,
+ y: 83,
+};
+
+export const GROUP_BLOCK_LAYOUT_DATA = {
+ clientId: 'e18249d9-ec06-4f54-b71e-6ec59be5213e',
+ width: 390,
+ height: 164,
+ x: 0,
+ y: 83,
+};