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

Add reusable block tab to inserter. #23296

Merged
merged 4 commits into from
Jul 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 0 additions & 30 deletions packages/block-editor/src/components/inserter/block-types-tab.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import { map, findIndex, flow, sortBy, groupBy, isEmpty } from 'lodash';
*/
import { __, _x, _n, sprintf } from '@wordpress/i18n';
import { withSpokenMessages } from '@wordpress/components';
import { addQueryArgs } from '@wordpress/url';
import { controlsRepeat } from '@wordpress/icons';
import { useMemo, useEffect } from '@wordpress/element';
import { useSelect } from '@wordpress/data';

Expand Down Expand Up @@ -60,12 +58,6 @@ export function BlockTypesTab( {
return items.slice( 0, MAX_SUGGESTED_ITEMS );
}, [ items ] );

const reusableItems = useMemo( () => {
return filteredItems.filter(
( { category } ) => category === 'reusable'
);
}, [ filteredItems ] );

const uncategorizedItems = useMemo( () => {
return filteredItems.filter( ( item ) => ! item.category );
}, [ filteredItems ] );
Expand Down Expand Up @@ -199,28 +191,6 @@ export function BlockTypesTab( {
);
} ) }

{ ! hasChildItems && !! reusableItems.length && (
<InserterPanel
className="block-editor-inserter__reusable-blocks-panel"
title={ __( 'Reusable' ) }
icon={ controlsRepeat }
>
<BlockTypesList
items={ reusableItems }
onSelect={ onSelectItem }
onHover={ onHover }
/>
<a
className="block-editor-inserter__manage-reusable-blocks"
href={ addQueryArgs( 'edit.php', {
post_type: 'wp_block',
} ) }
>
{ __( 'Manage all reusable blocks' ) }
</a>
</InserterPanel>
) }

<__experimentalInserterMenuExtension.Slot
fillProps={ {
onSelect: onSelectItem,
Expand Down
45 changes: 31 additions & 14 deletions packages/block-editor/src/components/inserter/menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import InserterSearchForm from './search-form';
import InserterPreviewPanel from './preview-panel';
import BlockTypesTab from './block-types-tab';
import BlockPatternsTabs from './block-patterns-tab';
import ReusableBlocksTab from './reusable-blocks-tab';
import useInsertionPoint from './hooks/use-insertion-point';
import InserterTabs from './tabs';

Expand All @@ -46,18 +47,20 @@ function InserterMenu( {
isAppender,
selectBlockOnInsert: __experimentalSelectBlockOnInsert,
} );
const { hasPatterns } = useSelect(
( select ) => {
const { getSettings } = select( 'core/block-editor' );
return {
hasPatterns: !! getSettings().__experimentalBlockPatterns
?.length,
};
},
[ isAppender, clientId, rootClientId ]
);
const { hasPatterns, hasReusableBlocks } = useSelect( ( select ) => {
const {
__experimentalBlockPatterns,
__experimentalReusableBlocks,
} = select( 'core/block-editor' ).getSettings();

return {
hasPatterns: !! __experimentalBlockPatterns?.length,
hasReusableBlocks: !! __experimentalReusableBlocks?.length,
ZebulanStanphill marked this conversation as resolved.
Show resolved Hide resolved
};
}, [] );

const showPatterns = ! destinationRootClientId && hasPatterns;

const onKeyDown = ( event ) => {
if (
includes(
Expand Down Expand Up @@ -106,6 +109,15 @@ function InserterMenu( {
<BlockPatternsTabs onInsert={ onInsert } filterValue={ filterValue } />
);

const reusableBlocksTab = (
<ReusableBlocksTab
rootClientId={ destinationRootClientId }
onInsert={ onInsert }
onHover={ onHover }
filterValue={ filterValue }
/>
);

// Disable reason (no-autofocus): The inserter menu is a modal display, not one which
// is always visible, and one which already incurs this behavior of autoFocus via
// Popover's focusOnMount.
Expand All @@ -125,17 +137,22 @@ function InserterMenu( {
onChange={ setFilterValue }
value={ filterValue }
/>
{ showPatterns && (
<InserterTabs>
{ ( showPatterns || hasReusableBlocks ) && (
<InserterTabs
showPatterns={ showPatterns }
showReusableBlocks={ hasReusableBlocks }
>
{ ( tab ) => {
if ( tab.name === 'blocks' ) {
return blocksTab;
} else if ( tab.name === 'patterns' ) {
return patternsTab;
}
return patternsTab;
return reusableBlocksTab;
} }
</InserterTabs>
) }
{ ! showPatterns && blocksTab }
{ ! showPatterns && ! hasReusableBlocks && blocksTab }
</div>
</div>
{ showInserterHelpPanel && hoveredItem && (
Expand Down
118 changes: 118 additions & 0 deletions packages/block-editor/src/components/inserter/reusable-blocks-tab.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**
* WordPress dependencies
*/
import { withSpokenMessages } from '@wordpress/components';
import { useMemo, useEffect } from '@wordpress/element';
import { __, _n, sprintf } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';

/**
* Internal dependencies
*/
import BlockTypesList from '../block-types-list';
import { searchBlockItems } from './search-items';
import InserterPanel from './panel';
import InserterNoResults from './no-results';
import useBlockTypesState from './hooks/use-block-types-state';

function ReusableBlocksList( {
debouncedSpeak,
filterValue,
onHover,
onInsert,
rootClientId,
} ) {
const [ items, categories, collections, onSelectItem ] = useBlockTypesState(
rootClientId,
onInsert
);

const filteredItems = useMemo( () => {
const reusableItems = items.filter(
( { category } ) => category === 'reusable'
);

if ( ! filterValue ) {
return reusableItems;
}
return searchBlockItems(
reusableItems,
categories,
collections,
filterValue
);
}, [ filterValue, items, categories, collections ] );

// Announce search results on change.
useEffect( () => {
const resultsFoundMessage = sprintf(
/* translators: %d: number of results. */
_n( '%d result found.', '%d results found.', filteredItems.length ),
filteredItems.length
);
debouncedSpeak( resultsFoundMessage );
}, [ filterValue, debouncedSpeak ] );

if ( filteredItems.length === 0 ) {
return <InserterNoResults />;
}

return (
<InserterPanel
title={
filterValue ? __( 'Search Results' ) : __( 'Reusable blocks' )
}
>
<BlockTypesList
items={ filteredItems }
onSelect={ onSelectItem }
onHover={ onHover }
/>
</InserterPanel>
);
}

// The unwrapped component is only exported for use by unit tests.
/**
* List of reusable blocks shown in the "Reusable" tab of the inserter.
*
* @param {Object} props Component props.
* @param {?string} props.rootClientId Client id of block to insert into.
* @param {Function} props.onInsert Callback to run when item is inserted.
* @param {Function} props.onHover Callback to run when item is hovered.
* @param {?string} props.filterValue Search term.
* @param {Function} props.debouncedSpeak Debounced speak function.
*
* @return {WPComponent} The component.
*/
export function ReusableBlocksTab( {
rootClientId,
onInsert,
onHover,
filterValue,
debouncedSpeak,
} ) {
return (
<>
<ReusableBlocksList
debouncedSpeak={ debouncedSpeak }
filterValue={ filterValue }
onHover={ onHover }
onInsert={ onInsert }
rootClientId={ rootClientId }
/>
<div className="block-editor-inserter__manage-reusable-blocks-container">
<a
className="block-editor-inserter__manage-reusable-blocks"
href={ addQueryArgs( 'edit.php', {
post_type: 'wp_block',
} ) }
>
{ __( 'Manage all reusable blocks' ) }
</a>
</div>
</>
);
}

export default withSpokenMessages( ReusableBlocksTab );
4 changes: 4 additions & 0 deletions packages/block-editor/src/components/inserter/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,10 @@ $block-inserter-tabs-height: 44px;
flex-shrink: 0;
}

.block-editor-inserter__manage-reusable-blocks-container {
padding: $grid-unit-20;
}

.block-editor-inserter__quick-inserter {
width: $block-inserter-width;
}
Expand Down
47 changes: 31 additions & 16 deletions packages/block-editor/src/components/inserter/tabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,38 @@
import { TabPanel } from '@wordpress/components';
import { __ } from '@wordpress/i18n';

function InserterTabs( { children } ) {
const blocksTab = {
name: 'blocks',
/* translators: Blocks tab title in the block inserter. */
title: __( 'Blocks' ),
};
const patternsTab = {
name: 'patterns',
/* translators: Patterns tab title in the block inserter. */
title: __( 'Patterns' ),
};
const reusableBlocksTab = {
name: 'reusable',
/* translators: Reusable blocks tab title in the block inserter. */
title: __( 'Reusable' ),
};

function InserterTabs( {
children,
showPatterns = false,
showReusableBlocks = false,
} ) {
const tabs = [ blocksTab ];

if ( showPatterns ) {
tabs.push( patternsTab );
}
if ( showReusableBlocks ) {
tabs.push( reusableBlocksTab );
}

return (
<TabPanel
className="block-editor-inserter__tabs"
tabs={ [
{
name: 'blocks',
/* translators: Blocks tab title in the block inserter. */
title: __( 'Blocks' ),
},
{
name: 'patterns',
/* translators: Patterns tab title in the block inserter. */
title: __( 'Patterns' ),
},
] }
>
<TabPanel className="block-editor-inserter__tabs" tabs={ tabs }>
{ children }
</TabPanel>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,25 +116,6 @@ describe( 'InserterMenu', () => {
assertNoResultsMessageNotToBePresent( container );
} );

it( 'should show reusable items in the reusable tab', () => {
const { container } = initializeAllClosedMenuState();
const reusableTabContent = container.querySelectorAll(
'.block-editor-inserter__panel-content'
)[ 5 ];
const reusableTabTitle = container.querySelectorAll(
'.block-editor-inserter__panel-title'
)[ 5 ];
const blocks = reusableTabContent.querySelectorAll(
'.block-editor-block-types-list__item-title'
);

expect( reusableTabTitle.textContent ).toBe( 'Reusable' );
expect( blocks ).toHaveLength( 1 );
expect( blocks[ 0 ].textContent ).toBe( 'My reusable block' );

assertNoResultsMessageNotToBePresent( container );
} );

it( 'should show the common category blocks', () => {
const { container } = initializeAllClosedMenuState();
const commonTabContent = container.querySelectorAll(
Expand Down Expand Up @@ -218,44 +199,17 @@ describe( 'InserterMenu', () => {
assertNoResultsMessageNotToBePresent( container );
} );

it( 'should allow searching for reusable blocks by title', () => {
const { container } = render(
<InserterBlockList filterValue="my reusable" />
);

const matchingCategories = container.querySelectorAll(
'.block-editor-inserter__panel-title'
);

expect( matchingCategories ).toHaveLength( 2 );
expect( matchingCategories[ 0 ].textContent ).toBe( 'Core' ); // "Core" namespace collection
expect( matchingCategories[ 1 ].textContent ).toBe( 'Reusable' );

const blocks = container.querySelectorAll(
'.block-editor-block-types-list__item-title'
);

// There are two buttons present for 1 total distinct result. The
// additional one accounts for the collection result (repeated).
expect( blocks ).toHaveLength( 2 );
expect( debouncedSpeak ).toHaveBeenCalledWith( '1 result found.' );
expect( blocks[ 0 ].textContent ).toBe( 'My reusable block' );
expect( blocks[ 1 ].textContent ).toBe( 'My reusable block' );

assertNoResultsMessageNotToBePresent( container );
} );

it( 'should speak after any change in search term', () => {
// The search result count should always be announced any time the user
// changes the search term, even if it results in the same count.
//
// See: https://github.com/WordPress/gutenberg/pull/22279#discussion_r423317161
const { rerender } = render(
<InserterBlockList filterValue="my reusab" />
<InserterBlockList filterValue="Advanced Para" />
);

rerender( <InserterBlockList filterValue="my reusable" /> );
rerender( <InserterBlockList filterValue="my reusable" /> );
rerender( <InserterBlockList filterValue="Advanced Paragraph" /> );
rerender( <InserterBlockList filterValue="Advanced Paragraph" /> );

expect( debouncedSpeak ).toHaveBeenCalledTimes( 2 );
expect( debouncedSpeak.mock.calls[ 0 ][ 0 ] ).toBe( '1 result found.' );
Expand Down
Loading