Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Inserter displays block collections #42405

Merged
merged 13 commits into from
Jul 15, 2022
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import {
Dimensions,
FlatList,
SectionList,
StyleSheet,
TouchableWithoutFeedback,
View,
Expand All @@ -24,7 +25,7 @@ const MIN_COL_NUM = 3;

export default function BlockTypesList( {
name,
items,
sections,
onSelect,
listProps,
initialNumToRender = 3,
Expand Down Expand Up @@ -80,33 +81,63 @@ export default function BlockTypesList( {
listProps.contentContainerStyle
);

const renderSection = ( { item } ) => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While testing these changes, I did not observe performance issues on my devices. However, I explored memoizing renderSection and its sibling render functions nonetheless. I did not observe much gain in doing so.

It appeared the majority of re-renders are triggered from listProps changing (listProps is a large set of attributes from BottomSheet), which refactoring listProps is likely outside of the scope of this PR.

That said, I welcome push back on this topic if we feel there are meaningful performance optimization we should put in place.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that this seems outside the scope of this PR. I spent some time measuring performance when adding a block for a separate project this week and believe I also saw listProps come up a lot as part of that, so seems like something that could be investigated separately 👍

return (
<TouchableWithoutFeedback accessible={ false }>
<FlatList
data={ item.list }
key={ `InserterUI-${ name }-${ numberOfColumns }` } // Re-render when numberOfColumns changes.
numColumns={ numberOfColumns }
ItemSeparatorComponent={ () => (
<TouchableWithoutFeedback accessible={ false }>
<View
style={
styles[ 'block-types-list__row-separator' ]
}
/>
</TouchableWithoutFeedback>
) }
scrollEnabled={ false }
renderItem={ renderListItem }
/>
</TouchableWithoutFeedback>
);
};

const renderListItem = ( { item } ) => {
return (
<InserterButton
item={ item }
itemWidth={ itemWidth }
maxWidth={ maxWidth }
onSelect={ onSelect }
/>
);
};

const renderSectionHeader = ( { section: { metadata } } ) => {
if ( ! metadata?.icon ) {
return null;
}

return (
<TouchableWithoutFeedback accessible={ false }>
<View style={ styles[ 'block-types-list__section-header' ] }>
{ metadata?.icon }
</View>
</TouchableWithoutFeedback>
);
};

return (
<FlatList
<SectionList
onLayout={ onLayout }
key={ `InserterUI-${ name }-${ numberOfColumns }` } // Re-render when numberOfColumns changes.
testID={ `InserterUI-${ name }` }
keyboardShouldPersistTaps="always"
numColumns={ numberOfColumns }
data={ items }
sections={ sections }
initialNumToRender={ initialNumToRender }
ItemSeparatorComponent={ () => (
<TouchableWithoutFeedback accessible={ false }>
<View
style={ styles[ 'block-types-list__row-separator' ] }
/>
</TouchableWithoutFeedback>
) }
keyExtractor={ ( item ) => item.id }
renderItem={ ( { item } ) => (
<InserterButton
{ ...{
item,
itemWidth,
maxWidth,
onSelect,
} }
/>
) }
renderItem={ renderSection }
renderSectionHeader={ renderSectionHeader }
{ ...listProps }
contentContainerStyle={ {
...contentContainerStyle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,10 @@
.block-types-list__column {
padding: $grid-unit-20;
}

.block-types-list__section-header {
flex-direction: row;
justify-content: center;
padding-bottom: 16;
padding-top: 32;
}
Original file line number Diff line number Diff line change
@@ -1,48 +1,69 @@
/**
* 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(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This useSelect was replaced with useBlockTypesState to further align the native inserter with the web. I discovered the latter within the web's BlockTypesTab component, and it appeared to be a hook specifically designed for this use case.

( 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, onSelectItem ] = useBlockTypesState(
rootClientId,
onSelect
);

const clipboardBlock = useClipboardBlock( rootClientId );
const filteredBlockTypes = filterInserterItems( rawBlockTypes );
const blockTypes = clipboardBlock
? [ clipboardBlock, ...filteredBlockTypes ]
: filteredBlockTypes;
const { items, trackBlockTypeSelected } =
useBlockTypeImpressions( blockTypes );

const handleSelect = ( ...args ) => {
trackBlockTypeSelected( ...args );
onSelect( ...args );
onSelectItem( ...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 (
<BlockTypesList
name="Blocks"
items={ items }
sections={ sections }
onSelect={ handleSelect }
listProps={ listProps }
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -23,10 +23,12 @@ function ReusableBlocksTab( { onSelect, rootClientId, listProps } ) {
[ rootClientId ]
);

const sections = [ createInserterSection( { key: 'reuseable', items } ) ];

return (
<BlockTypesList
name="ReusableBlocks"
items={ items }
sections={ sections }
onSelect={ onSelect }
listProps={ listProps }
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -51,7 +51,9 @@ function InserterSearchResults( {
<BlockTypesList
name="Blocks"
initialNumToRender={ isFullScreen ? 10 : 3 }
{ ...{ items, onSelect: handleSelect, listProps } }
sections={ [ createInserterSection( { key: 'search', items } ) ] }
onSelect={ handleSelect }
listProps={ listProps }
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
37 changes: 37 additions & 0 deletions packages/block-editor/src/components/inserter/test/utils.native.js
Original file line number Diff line number Diff line change
@@ -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.' );
} );
} );
11 changes: 11 additions & 0 deletions packages/block-editor/src/components/inserter/utils.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 } ],
};
}
1 change: 1 addition & 0 deletions packages/react-native-editor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down