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

Query Loop Block: Use BlockPatternsList component for Query block patterns modal #47366

Merged
merged 8 commits into from
Jan 31, 2023
1 change: 1 addition & 0 deletions packages/base-styles/_z-index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ $z-layers: (

// These next three share a stacking context
".block-library-template-part__selection-search": 2, // higher sticky element
".block-library-query-pattern__selection-search": 2, // higher sticky element

// These next two share a stacking context
".interface-complementary-area .components-panel" : 0, // lower scrolling content
Expand Down
63 changes: 4 additions & 59 deletions packages/block-library/src/query/edit/index.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,16 @@
/**
* WordPress dependencies
*/
import { useSelect, useDispatch } from '@wordpress/data';
import { useState, useMemo } from '@wordpress/element';
import {
BlockContextProvider,
store as blockEditorStore,
__experimentalBlockPatternSetup as BlockPatternSetup,
} from '@wordpress/block-editor';
import { Modal } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useSelect } from '@wordpress/data';
import { useState } from '@wordpress/element';
import { store as blockEditorStore } from '@wordpress/block-editor';

/**
* Internal dependencies
*/
import QueryContent from './query-content';
import QueryPlaceholder from './query-placeholder';
import {
useBlockNameForPatterns,
getTransformedBlocksFromPattern,
} from '../utils';
import PatternSelectionModal from './pattern-selection-modal';

const QueryEdit = ( props ) => {
const { clientId, attributes } = props;
Expand Down Expand Up @@ -52,50 +43,4 @@ const QueryEdit = ( props ) => {
);
};

function PatternSelectionModal( {
clientId,
attributes,
setIsPatternSelectionModalOpen,
} ) {
const { replaceBlock, selectBlock } = useDispatch( blockEditorStore );
const onBlockPatternSelect = ( blocks ) => {
const { newBlocks, queryClientIds } = getTransformedBlocksFromPattern(
blocks,
attributes
);
replaceBlock( clientId, newBlocks );
if ( queryClientIds[ 0 ] ) {
selectBlock( queryClientIds[ 0 ] );
}
};
// When we preview Query Loop blocks we should prefer the current
apeatling marked this conversation as resolved.
Show resolved Hide resolved
// block's postType, which is passed through block context.
const blockPreviewContext = useMemo(
() => ( {
previewPostType: attributes.query.postType,
} ),
[ attributes.query.postType ]
);
const blockNameForPatterns = useBlockNameForPatterns(
clientId,
attributes
);
return (
<Modal
className="block-editor-query-pattern__selection-modal"
title={ __( 'Choose a pattern' ) }
onRequestClose={ () => setIsPatternSelectionModalOpen( false ) }
>
<BlockContextProvider value={ blockPreviewContext }>
<BlockPatternSetup
blockName={ blockNameForPatterns }
clientId={ clientId }
onBlockPatternSelect={ onBlockPatternSelect }
showTitles={ true }
/>
</BlockContextProvider>
</Modal>
);
}

export default QueryEdit;
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* WordPress dependencies
*/
import { useState, useMemo } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import { Modal, SearchControl } from '@wordpress/components';
import { useAsyncList } from '@wordpress/compose';
import {
BlockContextProvider,
store as blockEditorStore,
__experimentalBlockPatternsList as BlockPatternsList,
} from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import {
useBlockNameForPatterns,
getTransformedBlocksFromPattern,
usePatterns,
} from '../utils';
import { searchPatterns } from '../../utils/search-patterns';

export default function PatternSelectionModal( {
clientId,
attributes,
setIsPatternSelectionModalOpen,
} ) {
const [ searchValue, setSearchValue ] = useState( '' );
const { replaceBlock, selectBlock } = useDispatch( blockEditorStore );
const onBlockPatternSelect = ( pattern, blocks ) => {
const { newBlocks, queryClientIds } = getTransformedBlocksFromPattern(
blocks,
attributes
);
replaceBlock( clientId, newBlocks );
if ( queryClientIds[ 0 ] ) {
selectBlock( queryClientIds[ 0 ] );
}
};
// When we preview Query Loop blocks we should prefer the current
// block's postType, which is passed through block context.
const blockPreviewContext = useMemo(
() => ( {
previewPostType: attributes.query.postType,
} ),
[ attributes.query.postType ]
);
const blockNameForPatterns = useBlockNameForPatterns(
clientId,
attributes
);
const blockPatterns = usePatterns( clientId, blockNameForPatterns );
const filteredBlockPatterns = useMemo( () => {
return searchPatterns( blockPatterns, searchValue );
}, [ blockPatterns, searchValue ] );
const shownBlockPatterns = useAsyncList( filteredBlockPatterns );

return (
<Modal
overlayClassName="block-library-query-pattern__selection-modal"
title={ __( 'Choose a pattern' ) }
onRequestClose={ () => setIsPatternSelectionModalOpen( false ) }
>
<div className="block-library-query-pattern__selection-content">
<div className="block-library-query-pattern__selection-search">
<SearchControl
__nextHasNoMarginBottom
onChange={ setSearchValue }
value={ searchValue }
label={ __( 'Search for patterns' ) }
placeholder={ __( 'Search' ) }
/>
</div>
<BlockContextProvider value={ blockPreviewContext }>
<BlockPatternsList
apeatling marked this conversation as resolved.
Show resolved Hide resolved
blockPatterns={ filteredBlockPatterns }
shownPatterns={ shownBlockPatterns }
onClickPattern={ onBlockPatternSelect }
/>
</BlockContextProvider>
</div>
</Modal>
);
}
17 changes: 6 additions & 11 deletions packages/block-library/src/query/edit/query-toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ import {
import { useInstanceId } from '@wordpress/compose';
import { __ } from '@wordpress/i18n';
import { settings, list, grid } from '@wordpress/icons';
import { useSelect } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor';

/**
* Internal dependencies
*/
import { usePatterns } from '../utils';

export default function QueryToolbar( {
attributes: { query, displayLayout },
Expand All @@ -22,15 +25,7 @@ export default function QueryToolbar( {
name,
clientId,
} ) {
const hasPatterns = useSelect(
( select ) => {
const { getBlockRootClientId, getPatternsByBlockTypes } =
select( blockEditorStore );
const rootClientId = getBlockRootClientId( clientId );
return !! getPatternsByBlockTypes( name, rootClientId ).length;
},
[ name, clientId ]
);
const hasPatterns = !! usePatterns( clientId, name ).length;
const maxPageInputId = useInstanceId(
QueryToolbar,
'blocks-query-pagination-max-page-input'
Expand Down
40 changes: 21 additions & 19 deletions packages/block-library/src/query/editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,30 @@
}
}

.block-editor-query-pattern__selection-modal .components-modal__content {
padding: 0;
margin-bottom: $header-height; // $header-height to accommodate bottom toolbar
&::before {
margin-bottom: 0;
}
}

.block-editor-query-pattern__selection-modal {
.block-library-query-pattern__selection-modal {
// To keep modal dimensions consistent as subsections are navigated, width
// and height are used instead of max-(width/height).
@include break-small() {
width: calc(100% - #{ $grid-unit-20 * 2 });
height: calc(100% - #{ $header-height * 2 });
}
@include break-medium() {
width: $break-medium - $grid-unit-20 * 2;
.components-modal__frame {
@include break-small() {
width: calc(100% - #{$grid-unit-20 * 2});
height: calc(100% - #{$header-height * 2});
}

@include break-medium() {
width: $break-medium - $grid-unit-20 * 2;
}

@include break-large() {
height: 70%;
}
}
@include break-large() {
height: 80%;
width: 80%;
max-height: none;

.block-library-query-pattern__selection-search {
background: $white;
position: sticky;
top: 0;
padding: $grid-unit-20 0;
z-index: z-index(".block-library-query-pattern__selection-search");
}
}

Expand Down
19 changes: 19 additions & 0 deletions packages/block-library/src/query/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -313,3 +313,22 @@ export function useScopedBlockVariations( attributes ) {
}, [ activeVariationName, blockVariations ] );
return variations;
}

/**
* Hook that returns the block patterns for a specific block type.
*
* @param {string} clientId The block's client ID.
* @param {string} name The block type name.
* @return {Object[]} An array of valid block patterns.
*/
export const usePatterns = ( clientId, name ) => {
return useSelect(
( select ) => {
const { getBlockRootClientId, getPatternsByBlockTypes } =
select( blockEditorStore );
const rootClientId = getBlockRootClientId( clientId );
return getPatternsByBlockTypes( name, rootClientId );
},
[ name, clientId ]
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
useCreateTemplatePartFromBlocks,
} from './utils/hooks';
import { createTemplatePartId } from './utils/create-template-part-id';
import { searchPatterns } from './utils/search';
import { searchPatterns } from '../../utils/search-patterns';

export default function TemplatePartSelectionModal( {
setAttributes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import removeAccents from 'remove-accents';
*
* @return {string} The normalized search input.
*/
function normalizeSearchInput( input = '' ) {
export function normalizeSearchInput( input = '' ) {
// Disregard diacritics.
input = removeAccents( input );

Expand All @@ -27,7 +27,7 @@ function normalizeSearchInput( input = '' ) {
* @param {string} searchValue Search term
* @return {number} A pattern search rank
*/
function getPatternSearchRank( pattern, searchValue ) {
export function getPatternSearchRank( pattern, searchValue ) {
const normalizedSearchValue = normalizeSearchInput( searchValue );
const normalizedTitle = normalizeSearchInput( pattern.title );

Expand Down
58 changes: 58 additions & 0 deletions packages/block-library/src/utils/test/search-patterns.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Internal dependencies
*/
import {
normalizeSearchInput,
getPatternSearchRank,
searchPatterns,
} from '../search-patterns';

describe( 'normalizeSearchInput', () => {
it( 'should remove accents', () => {
expect( normalizeSearchInput( 'café' ) ).toBe( 'cafe' );
} );

it( 'should trim and lowercase', () => {
expect( normalizeSearchInput( ' Foo ' ) ).toBe( 'foo' );
} );
} );

describe( 'getPatternSearchRank', () => {
it( 'should give a high rank to exact matches', () => {
const pattern = { title: 'Foo' };
expect( getPatternSearchRank( pattern, 'Foo' ) ).toBe( 30 );
} );

it( 'should give a high rank to prefix matches', () => {
const pattern = { title: 'Foo' };
expect( getPatternSearchRank( pattern, 'Fo' ) ).toBe( 20 );
} );

it( 'should give a low rank to no matches', () => {
const pattern = { title: 'Foo' };
expect( getPatternSearchRank( pattern, 'Bar' ) ).toBe( 0 );
} );
} );

describe( 'searchPatterns', () => {
it( 'should return all patterns if no search term is provided', () => {
const patterns = [ { title: 'Foo' }, { title: 'Bar' } ];
expect( searchPatterns( patterns, '' ) ).toEqual( patterns );
} );

it( 'should return an empty array if no patterns are provided', () => {
expect( searchPatterns( [], 'Foo' ) ).toEqual( [] );
} );

it( 'should return an empty array if no patterns match', () => {
const patterns = [ { title: 'Foo' }, { title: 'Bar' } ];
expect( searchPatterns( patterns, 'Baz' ) ).toEqual( [] );
} );

it( 'should return the matching patterns', () => {
const patterns = [ { title: 'Foo' }, { title: 'Bar' } ];
expect( searchPatterns( patterns, 'Foo' ) ).toEqual( [
patterns[ 0 ],
] );
} );
} );
Loading