From a7181f260d3e48d9c4f39b0d41d83f99f491519f Mon Sep 17 00:00:00 2001
From: David Calhoun <438664+dcalhoun@users.noreply.github.com>
Date: Fri, 15 Jul 2022 11:01:59 -0500
Subject: [PATCH] feat: Inserter displays block collections (#42405)
* feat: Inserter display block collections
Collections of blocks regsitered via `registerBlockCollection` now
appear at the end of the block inserter enabling additional organization
of blocks.
* test: Add test for createInserterSection utility
* fix: Update reusable blocks list data structure
The `BlockTypesList` now expects `sections` to support the `SectionList`
used within it for displaying block collections. Prior to this change,
an error was thrown.
* test: Fix failure by expanding selector mock
The `BlockTypesTab` now relies upon additional store selectors. The lack
of mocks for these selectors caused test failures.
* docs: Add changelog entry
* refactor: Simplify and mirror web implementation
The code contained redundant selectors against the store for the block
type list. This change refactors the native inserter to match the web
counterpart, utilizing the selector leveraged by the web.
* refactor: Remove unnecessary object spread
While verbose, passing explicit props is likely more orthodox and
straightforward to interpret.
* fix: Remove broken keyExtractor
The key for this component's sections is now `key`, not `id`, which
resulted in undefined key values returned here. However, the default
`keyExtractor` already searches for `key` and then `id`, so this can be
safely removed.
* fix: Revert utilization of onSelect from useBlockTypesState
The block created from within this callback appears to be in a format
that is incompatible with the native editor at this time. Specifically,
this appeared to break Embed block variants, e.g. Vimeo. A future
improvement would be to investigate this further in hopes of aligning
web and native.
* chore: Add background to section headers
Improve legibility of section headers whenever blocks scroll beneath the
header.
* chore: Display collection title to improve dark mode support
Leverage merely an icon with text made dark mode support more
challenging. Rendering text allows for easy swapping of color schemes.
* fix: Set collection title text color
* fix: Adjust horizontal spacing for collection header
---
.../block-types-list/index.native.js | 100 +++++++++++++-----
.../block-types-list/style.native.scss | 18 ++++
.../inserter/block-types-tab.native.js | 63 +++++++----
.../inserter/reusable-blocks-tab.native.js | 6 +-
.../inserter/search-results.native.js | 6 +-
.../inserter/test/block-types-tab.native.js | 2 +
.../components/inserter/test/utils.native.js | 37 +++++++
.../src/components/inserter/utils.native.js | 11 ++
packages/react-native-editor/CHANGELOG.md | 1 +
9 files changed, 195 insertions(+), 49 deletions(-)
create mode 100644 packages/block-editor/src/components/inserter/test/utils.native.js
diff --git a/packages/block-editor/src/components/block-types-list/index.native.js b/packages/block-editor/src/components/block-types-list/index.native.js
index 03a3d7e62eb686..d62987d408aeaf 100644
--- a/packages/block-editor/src/components/block-types-list/index.native.js
+++ b/packages/block-editor/src/components/block-types-list/index.native.js
@@ -4,7 +4,9 @@
import {
Dimensions,
FlatList,
+ SectionList,
StyleSheet,
+ Text,
TouchableWithoutFeedback,
View,
} from 'react-native';
@@ -13,7 +15,11 @@ import {
* WordPress dependencies
*/
import { useState, useEffect } from '@wordpress/element';
-import { BottomSheet, InserterButton } from '@wordpress/components';
+import { BottomSheet, Gradient, InserterButton } from '@wordpress/components';
+import {
+ usePreferredColorScheme,
+ usePreferredColorSchemeStyle,
+} from '@wordpress/compose';
/**
* Internal dependencies
@@ -24,7 +30,7 @@ const MIN_COL_NUM = 3;
export default function BlockTypesList( {
name,
- items,
+ sections,
onSelect,
listProps,
initialNumToRender = 3,
@@ -80,33 +86,79 @@ export default function BlockTypesList( {
listProps.contentContainerStyle
);
+ const renderSection = ( { item } ) => {
+ return (
+
+ (
+
+
+
+ ) }
+ scrollEnabled={ false }
+ renderItem={ renderListItem }
+ />
+
+ );
+ };
+
+ const renderListItem = ( { item } ) => {
+ return (
+
+ );
+ };
+
+ const colorScheme = usePreferredColorScheme();
+ const sectionHeaderGradientValue =
+ colorScheme === 'light'
+ ? 'linear-gradient(#fff 70%, rgba(255, 255, 255, 0))'
+ : 'linear-gradient(#2e2e2e 70%, rgba(46, 46, 46, 0))';
+ const sectionHeaderTitleStyles = usePreferredColorSchemeStyle(
+ styles[ 'block-types-list__section-header-title' ],
+ styles[ 'block-types-list__section-header-title--dark' ]
+ );
+
+ const renderSectionHeader = ( { section: { metadata } } ) => {
+ if ( ! metadata?.icon || ! metadata?.title ) {
+ return null;
+ }
+
+ return (
+
+
+ { metadata?.icon }
+
+ { metadata?.title }
+
+
+
+ );
+ };
+
return (
- (
-
-
-
- ) }
- keyExtractor={ ( item ) => item.id }
- renderItem={ ( { item } ) => (
-
- ) }
+ renderItem={ renderSection }
+ renderSectionHeader={ renderSectionHeader }
{ ...listProps }
contentContainerStyle={ {
...contentContainerStyle,
diff --git a/packages/block-editor/src/components/block-types-list/style.native.scss b/packages/block-editor/src/components/block-types-list/style.native.scss
index a9a3b4171ba18b..434f46966d1201 100644
--- a/packages/block-editor/src/components/block-types-list/style.native.scss
+++ b/packages/block-editor/src/components/block-types-list/style.native.scss
@@ -5,3 +5,21 @@
.block-types-list__column {
padding: $grid-unit-20;
}
+
+.block-types-list__section-header {
+ align-items: center;
+ flex-direction: row;
+ justify-content: center;
+ padding-bottom: 16;
+ padding-top: 32;
+}
+
+.block-types-list__section-header-title {
+ color: $black;
+ font-size: 16px;
+ margin-left: 10px;
+}
+
+.block-types-list__section-header-title--dark {
+ color: $white;
+}
diff --git a/packages/block-editor/src/components/inserter/block-types-tab.native.js b/packages/block-editor/src/components/inserter/block-types-tab.native.js
index 39abdf7ff280f1..97455915100840 100644
--- a/packages/block-editor/src/components/inserter/block-types-tab.native.js
+++ b/packages/block-editor/src/components/inserter/block-types-tab.native.js
@@ -1,36 +1,29 @@
/**
* WordPress dependencies
*/
-import { useSelect } from '@wordpress/data';
+import { useMemo } from '@wordpress/element';
/**
* Internal dependencies
*/
import BlockTypesList from '../block-types-list';
import useClipboardBlock from './hooks/use-clipboard-block';
-import { store as blockEditorStore } from '../../store';
import useBlockTypeImpressions from './hooks/use-block-type-impressions';
-import { filterInserterItems } from './utils';
+import { createInserterSection, filterInserterItems } from './utils';
+import useBlockTypesState from './hooks/use-block-types-state';
-function BlockTypesTab( { onSelect, rootClientId, listProps } ) {
- const clipboardBlock = useClipboardBlock( rootClientId );
-
- const { blockTypes } = useSelect(
- ( select ) => {
- const { getInserterItems } = select( blockEditorStore );
- const blockItems = filterInserterItems(
- getInserterItems( rootClientId )
- );
+const getBlockNamespace = ( item ) => item.name.split( '/' )[ 0 ];
- return {
- blockTypes: clipboardBlock
- ? [ clipboardBlock, ...blockItems ]
- : blockItems,
- };
- },
- [ rootClientId ]
+function BlockTypesTab( { onSelect, rootClientId, listProps } ) {
+ const [ rawBlockTypes, , collections ] = useBlockTypesState(
+ rootClientId,
+ onSelect
);
-
+ const clipboardBlock = useClipboardBlock( rootClientId );
+ const filteredBlockTypes = filterInserterItems( rawBlockTypes );
+ const blockTypes = clipboardBlock
+ ? [ clipboardBlock, ...filteredBlockTypes ]
+ : filteredBlockTypes;
const { items, trackBlockTypeSelected } =
useBlockTypeImpressions( blockTypes );
@@ -39,10 +32,38 @@ function BlockTypesTab( { onSelect, rootClientId, listProps } ) {
onSelect( ...args );
};
+ const collectionSections = useMemo( () => {
+ const result = [];
+ Object.keys( collections ).forEach( ( namespace ) => {
+ const data = items.filter(
+ ( item ) => getBlockNamespace( item ) === namespace
+ );
+ if ( data.length > 0 ) {
+ result.push(
+ createInserterSection( {
+ key: `collection-${ namespace }`,
+ metadata: {
+ icon: collections[ namespace ].icon,
+ title: collections[ namespace ].title,
+ },
+ items: data,
+ } )
+ );
+ }
+ } );
+
+ return result;
+ }, [ items, collections ] );
+
+ const sections = [
+ createInserterSection( { key: 'default', items } ),
+ ...collectionSections,
+ ];
+
return (
diff --git a/packages/block-editor/src/components/inserter/reusable-blocks-tab.native.js b/packages/block-editor/src/components/inserter/reusable-blocks-tab.native.js
index 19c629b0d4ea08..825169e17315f8 100644
--- a/packages/block-editor/src/components/inserter/reusable-blocks-tab.native.js
+++ b/packages/block-editor/src/components/inserter/reusable-blocks-tab.native.js
@@ -8,7 +8,7 @@ import { useSelect } from '@wordpress/data';
*/
import BlockTypesList from '../block-types-list';
import { store as blockEditorStore } from '../../store';
-import { filterInserterItems } from './utils';
+import { createInserterSection, filterInserterItems } from './utils';
function ReusableBlocksTab( { onSelect, rootClientId, listProps } ) {
const { items } = useSelect(
@@ -23,10 +23,12 @@ function ReusableBlocksTab( { onSelect, rootClientId, listProps } ) {
[ rootClientId ]
);
+ const sections = [ createInserterSection( { key: 'reuseable', items } ) ];
+
return (
diff --git a/packages/block-editor/src/components/inserter/search-results.native.js b/packages/block-editor/src/components/inserter/search-results.native.js
index f224ff78be17ae..a32c6ecf1a1c31 100644
--- a/packages/block-editor/src/components/inserter/search-results.native.js
+++ b/packages/block-editor/src/components/inserter/search-results.native.js
@@ -11,7 +11,7 @@ import BlockTypesList from '../block-types-list';
import InserterNoResults from './no-results';
import { store as blockEditorStore } from '../../store';
import useBlockTypeImpressions from './hooks/use-block-type-impressions';
-import { filterInserterItems } from './utils';
+import { createInserterSection, filterInserterItems } from './utils';
function InserterSearchResults( {
filterValue,
@@ -51,7 +51,9 @@ function InserterSearchResults( {
);
}
diff --git a/packages/block-editor/src/components/inserter/test/block-types-tab.native.js b/packages/block-editor/src/components/inserter/test/block-types-tab.native.js
index b9f82135fd7178..925570130359a6 100644
--- a/packages/block-editor/src/components/inserter/test/block-types-tab.native.js
+++ b/packages/block-editor/src/components/inserter/test/block-types-tab.native.js
@@ -18,6 +18,8 @@ jest.mock( '../hooks/use-clipboard-block' );
jest.mock( '@wordpress/data/src/components/use-select' );
const selectMock = {
+ getCategories: jest.fn().mockReturnValue( [] ),
+ getCollections: jest.fn().mockReturnValue( [] ),
getInserterItems: jest.fn().mockReturnValue( [] ),
canInsertBlockType: jest.fn(),
getBlockType: jest.fn(),
diff --git a/packages/block-editor/src/components/inserter/test/utils.native.js b/packages/block-editor/src/components/inserter/test/utils.native.js
new file mode 100644
index 00000000000000..d1b8cb57b8f40d
--- /dev/null
+++ b/packages/block-editor/src/components/inserter/test/utils.native.js
@@ -0,0 +1,37 @@
+/**
+ * Internal dependencies
+ */
+import { createInserterSection } from '../utils';
+
+describe( 'createInserterSection', () => {
+ it( 'returns the expected object shape', () => {
+ const key = 'mock-1';
+ const items = [ 1, 2, 3 ];
+ const metadata = { icon: 'icon-mock', title: 'Title Mock' };
+
+ expect( createInserterSection( { key, metadata, items } ) ).toEqual(
+ expect.objectContaining( {
+ metadata,
+ data: [ { key, list: items } ],
+ } )
+ );
+ } );
+
+ it( 'return always includes metadata', () => {
+ const key = 'mock-1';
+ const items = [ 1, 2, 3 ];
+
+ expect( createInserterSection( { key, items } ) ).toEqual(
+ expect.objectContaining( {
+ metadata: {},
+ data: [ { key, list: items } ],
+ } )
+ );
+ } );
+
+ it( 'requires a unique key', () => {
+ expect( () => {
+ createInserterSection( { items: [ 1, 2, 3 ] } );
+ } ).toThrow( 'A unique key for the section must be provided.' );
+ } );
+} );
diff --git a/packages/block-editor/src/components/inserter/utils.native.js b/packages/block-editor/src/components/inserter/utils.native.js
index b7c77c7e15509f..e8959c29cfe7f0 100644
--- a/packages/block-editor/src/components/inserter/utils.native.js
+++ b/packages/block-editor/src/components/inserter/utils.native.js
@@ -33,3 +33,14 @@ export function filterInserterItems(
blockAllowed( block, { onlyReusable, allowReusable } )
);
}
+
+export function createInserterSection( { key, metadata = {}, items } ) {
+ if ( ! key ) {
+ throw new Error( 'A unique key for the section must be provided.' );
+ }
+
+ return {
+ metadata,
+ data: [ { key, list: items } ],
+ };
+}
diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md
index 1f10d1077c5deb..bb401e0ec9984a 100644
--- a/packages/react-native-editor/CHANGELOG.md
+++ b/packages/react-native-editor/CHANGELOG.md
@@ -12,6 +12,7 @@ For each user feature we should also add a importance categorization label to i
## Unreleased
- [*] Add React Native FastImage [#42009]
+- [*] Block inserter displays block collections [#42405]
## 1.79.0
- [*] Add 'Insert from URL' option to Video block [#41493]