diff --git a/docs/reference-guides/data/data-core.md b/docs/reference-guides/data/data-core.md index 95401834ad4391..a3d6bd5b6f392c 100644 --- a/docs/reference-guides/data/data-core.md +++ b/docs/reference-guides/data/data-core.md @@ -403,6 +403,18 @@ _Returns_ - `Optional< any >`: The edit. +### getUserPatternCategories + +Retrieve the registered user pattern categories. + +_Parameters_ + +- _state_ `State`: Data state. + +_Returns_ + +- `UserPatternCategories`: User patterns category array and map keyed by id. + ### getUserQueryResults Returns all the users returned by a query ID. diff --git a/lib/compat/wordpress-6.4/block-patterns.php b/lib/compat/wordpress-6.4/block-patterns.php index aa4c5ef378bc7a..3a0a9ed1a9e52e 100644 --- a/lib/compat/wordpress-6.4/block-patterns.php +++ b/lib/compat/wordpress-6.4/block-patterns.php @@ -16,20 +16,19 @@ */ function gutenberg_register_taxonomy_patterns() { $args = array( - array( - 'public' => false, - 'hierarchical' => false, - 'labels' => array( - 'name' => _x( 'Pattern Categories', 'taxonomy general name' ), - 'singular_name' => _x( 'Pattern Category', 'taxonomy singular name' ), - ), - 'query_var' => false, - 'rewrite' => false, - 'show_ui' => false, - '_builtin' => true, - 'show_in_nav_menus' => false, - 'show_in_rest' => true, + 'public' => true, + 'hierarchical' => false, + 'labels' => array( + 'name' => _x( 'Pattern Categories', 'taxonomy general name' ), + 'singular_name' => _x( 'Pattern Category', 'taxonomy singular name' ), ), + 'query_var' => false, + 'rewrite' => false, + 'show_ui' => true, + '_builtin' => true, + 'show_in_nav_menus' => false, + 'show_in_rest' => true, + 'show_admin_column' => true, ); register_taxonomy( 'wp_pattern_category', array( 'wp_block' ), $args ); } diff --git a/package-lock.json b/package-lock.json index 6786f1006a98b7..124be7f72bd69c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56023,6 +56023,7 @@ "@wordpress/core-data": "file:../core-data", "@wordpress/data": "file:../data", "@wordpress/element": "file:../element", + "@wordpress/html-entities": "file:../html-entities", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", "@wordpress/notices": "file:../notices", @@ -68574,6 +68575,7 @@ "@wordpress/core-data": "file:../core-data", "@wordpress/data": "file:../data", "@wordpress/element": "file:../element", + "@wordpress/html-entities": "file:../html-entities", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", "@wordpress/notices": "file:../notices", diff --git a/packages/block-editor/src/components/block-patterns-list/index.js b/packages/block-editor/src/components/block-patterns-list/index.js index ec2aad3da2b7a4..302fe731d21875 100644 --- a/packages/block-editor/src/components/block-patterns-list/index.js +++ b/packages/block-editor/src/components/block-patterns-list/index.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * WordPress dependencies */ @@ -8,9 +13,11 @@ import { __unstableUseCompositeState as useCompositeState, __unstableCompositeItem as CompositeItem, Tooltip, + __experimentalHStack as HStack, } from '@wordpress/components'; import { useInstanceId } from '@wordpress/compose'; import { __ } from '@wordpress/i18n'; +import { Icon, symbol } from '@wordpress/icons'; /** * Internal dependencies @@ -63,14 +70,20 @@ function BlockPattern( { } } > { onClick( pattern, blocks ); onHover?.( null ); @@ -91,11 +104,23 @@ function BlockPattern( { blocks={ blocks } viewportWidth={ viewportWidth } /> - { ! showTooltip && ( -
- { pattern.title } -
- ) } + + + { pattern.id && ! pattern.syncStatus && ( +
+ +
+ ) } + { ( ! showTooltip || pattern.id ) && ( +
+ { pattern.title } +
+ ) } +
+ { !! pattern.description && ( { pattern.description } diff --git a/packages/block-editor/src/components/block-patterns-list/style.scss b/packages/block-editor/src/components/block-patterns-list/style.scss index ab80fc71d36dfe..e3b38deff5ef7a 100644 --- a/packages/block-editor/src/components/block-patterns-list/style.scss +++ b/packages/block-editor/src/components/block-patterns-list/style.scss @@ -11,7 +11,7 @@ min-height: 100px; } - &[draggable="true"] .block-editor-block-preview__container { + &[draggable="true"] { cursor: grab; } } @@ -27,22 +27,39 @@ } .block-editor-block-patterns-list__item-title { - padding-top: $grid-unit-10; - font-size: 12px; - text-align: center; + text-align: left; + flex-grow: 1; } &:hover .block-editor-block-preview__container { - box-shadow: 0 0 0 2px var(--wp-admin-theme-color); + box-shadow: 0 0 0 2px $gray-900; } &:focus .block-editor-block-preview__container { - @include button-style-outset__focus(var(--wp-admin-theme-color)); + @include button-style-outset__focus($gray-900); } + &.block-editor-block-patterns-list__list-item-synced { + &:hover, + &:focus { + .block-editor-block-preview__container { + box-shadow: + 0 0 0 2px var(--wp-block-synced-color), + 0 15px 25px rgb(0 0 0 / 7%); + } + } + } + + .block-editor-patterns__pattern-details { + align-items: center; + margin-top: $grid-unit-10; + } - &:hover .block-editor-block-patterns-list__item-title, - &:focus .block-editor-block-patterns-list__item-title { - color: var(--wp-admin-theme-color); + .block-editor-patterns__pattern-icon-wrapper { + min-width: 24px; + height: 24px; + .block-editor-patterns__pattern-icon { + fill: var(--wp-block-synced-color); + } } } diff --git a/packages/block-editor/src/components/block-patterns-paging/index.js b/packages/block-editor/src/components/block-patterns-paging/index.js new file mode 100644 index 00000000000000..610ff304e186ec --- /dev/null +++ b/packages/block-editor/src/components/block-patterns-paging/index.js @@ -0,0 +1,92 @@ +/** + * WordPress dependencies + */ +import { + __experimentalVStack as VStack, + __experimentalHStack as HStack, + __experimentalText as Text, + Button, +} from '@wordpress/components'; +import { __, _x, _n, sprintf } from '@wordpress/i18n'; + +export default function Pagination( { + currentPage, + numPages, + changePage, + totalItems, +} ) { + return ( + + + { + // translators: %s: Total number of patterns. + sprintf( + // translators: %s: Total number of patterns. + _n( '%s item', '%s items', totalItems ), + totalItems + ) + } + + + + + + + + { sprintf( + // translators: %1$s: Current page number, %2$s: Total number of pages. + _x( '%1$s of %2$s', 'paging' ), + currentPage, + numPages + ) } + + + + + + + + ); +} diff --git a/packages/block-editor/src/components/block-patterns-paging/style.scss b/packages/block-editor/src/components/block-patterns-paging/style.scss new file mode 100644 index 00000000000000..7de651f1511b65 --- /dev/null +++ b/packages/block-editor/src/components/block-patterns-paging/style.scss @@ -0,0 +1,42 @@ +.block-editor-patterns__grid-pagination { + border-top: 1px solid $gray-800; + padding: $grid-unit-05; + + .components-button.is-tertiary { + width: auto; + height: $button-size-compact; + justify-content: center; + + &:disabled { + color: $gray-600; + background: none; + } + + &:hover:not(:disabled) { + color: $white; + background-color: $gray-700; + } + } +} + +.show-icon-labels { + .block-editor-patterns__grid-pagination { + flex-direction: column; + .block-editor-patterns__grid-pagination-previous, + .block-editor-patterns__grid-pagination-next { + flex-direction: column; + } + .components-button { + width: auto; + // Hide the button icons when labels are set to display... + span { + display: none; + } + // ... and display labels. + // Uses ::before as ::after is already used for active tab styling. + &::before { + content: attr(aria-label); + } + } + } +} diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 5c9f640e64b222..9d8b1e9490a006 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -68,6 +68,9 @@ function bubbleEvent( event, Constructor, frame ) { function useBubbleEvents( iframeDocument ) { return useRefEffect( ( body ) => { const { defaultView } = iframeDocument; + if ( ! defaultView ) { + return; + } const { frameElement } = defaultView; const eventTypes = [ 'dragover', 'mousemove' ]; const handlers = {}; diff --git a/packages/block-editor/src/components/inserter/block-patterns-explorer/explorer.js b/packages/block-editor/src/components/inserter/block-patterns-explorer/explorer.js index 914177941821f3..e795a70679aa14 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-explorer/explorer.js +++ b/packages/block-editor/src/components/inserter/block-patterns-explorer/explorer.js @@ -10,25 +10,37 @@ import { __ } from '@wordpress/i18n'; */ import PatternExplorerSidebar from './sidebar'; import PatternList from './patterns-list'; +import { usePatternsCategories } from '../block-patterns-tab'; + +function PatternsExplorer( { initialCategory, rootClientId } ) { + const [ searchValue, setSearchValue ] = useState( '' ); + const [ patternSourceFilter, setPatternSourceFilter ] = useState( 'all' ); -function PatternsExplorer( { initialCategory, patternCategories } ) { - const [ filterValue, setFilterValue ] = useState( '' ); const [ selectedCategory, setSelectedCategory ] = useState( initialCategory?.name ); + + const patternCategories = usePatternsCategories( + rootClientId, + patternSourceFilter + ); + return (
); diff --git a/packages/block-editor/src/components/inserter/block-patterns-explorer/patterns-list.js b/packages/block-editor/src/components/inserter/block-patterns-explorer/patterns-list.js index fda1a00c1a07dc..a33ceba422f686 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-explorer/patterns-list.js +++ b/packages/block-editor/src/components/inserter/block-patterns-explorer/patterns-list.js @@ -1,9 +1,9 @@ /** * WordPress dependencies */ -import { useMemo, useEffect } from '@wordpress/element'; +import { useMemo, useEffect, useRef, useState } from '@wordpress/element'; import { _n, sprintf } from '@wordpress/i18n'; -import { useDebounce, useAsyncList } from '@wordpress/compose'; +import { useDebounce } from '@wordpress/compose'; import { __experimentalHeading as Heading } from '@wordpress/components'; import { speak } from '@wordpress/a11y'; @@ -11,18 +11,37 @@ import { speak } from '@wordpress/a11y'; * Internal dependencies */ import BlockPatternsList from '../../block-patterns-list'; -import InserterNoResults from '../no-results'; import useInsertionPoint from '../hooks/use-insertion-point'; import usePatternsState from '../hooks/use-patterns-state'; import InserterListbox from '../../inserter-listbox'; import { searchItems } from '../search-items'; +import BlockPatternsPaging from '../../block-patterns-paging'; +import usePatternsPaging from '../hooks/use-patterns-paging'; +import { allPatternsCategory, isPatternFiltered } from '../block-patterns-tab'; +import { BlockPatternsSyncFilter } from '../block-patterns-sync-filter'; +import { + PATTERN_TYPES, + PATTERN_SOURCE_FILTERS, +} from '../block-patterns-source-filter'; -const INITIAL_INSERTER_RESULTS = 2; - -function PatternsListHeader( { filterValue, filteredBlockPatternsLength } ) { +function PatternsListHeader( { + filterValue, + filteredBlockPatternsLength, + selectedCategory, + patternCategories, +} ) { if ( ! filterValue ) { return null; } + let filter = filterValue; + if ( selectedCategory !== allPatternsCategory.name ) { + const category = patternCategories.find( + ( patternCategory ) => patternCategory.name === selectedCategory + ); + if ( category ) { + filter = `${ filter } - ${ category?.label }`; + } + } return ( ); } -function PatternList( { filterValue, selectedCategory, patternCategories } ) { +function PatternList( { + searchValue, + patternSourceFilter, + selectedCategory, + patternCategories, +} ) { + const [ patternSyncFilter, setPatternSyncFilter ] = useState( 'all' ); + const container = useRef(); const debouncedSpeak = useDebounce( speak, 500 ); const [ destinationRootClientId, onInsertBlocks ] = useInsertionPoint( { shouldFocusBlock: true, } ); - const [ allPatterns, , onSelectBlockPattern ] = usePatternsState( + const { patterns: allPatterns, onClickPattern } = usePatternsState( onInsertBlocks, destinationRootClientId ); @@ -62,30 +88,54 @@ function PatternList( { filterValue, selectedCategory, patternCategories } ) { ); const filteredBlockPatterns = useMemo( () => { - if ( ! filterValue ) { - return allPatterns.filter( ( pattern ) => - selectedCategory === 'uncategorized' - ? ! pattern.categories?.length || - pattern.categories.every( - ( category ) => - ! registeredPatternCategories.includes( - category - ) - ) - : pattern.categories?.includes( selectedCategory ) - ); + const filteredPatterns = allPatterns.filter( ( pattern ) => { + if ( + isPatternFiltered( + pattern, + patternSourceFilter, + patternSyncFilter + ) + ) { + return false; + } + + if ( selectedCategory === allPatternsCategory.name ) { + return true; + } + + if ( selectedCategory === 'uncategorized' ) { + const hasKnownCategory = pattern.categories.some( + ( category ) => + registeredPatternCategories.includes( category ) + ); + + return ! pattern.categories?.length || ! hasKnownCategory; + } + + return pattern.categories?.includes( selectedCategory ); + } ); + + if ( ! searchValue ) { + return filteredPatterns; } - return searchItems( allPatterns, filterValue ); + + return searchItems( + filteredPatterns, + searchValue, + patternSourceFilter + ); }, [ - filterValue, + searchValue, + patternSourceFilter, allPatterns, selectedCategory, registeredPatternCategories, + patternSyncFilter, ] ); // Announce search results on change. useEffect( () => { - if ( ! filterValue ) { + if ( ! searchValue ) { return; } const count = filteredBlockPatterns.length; @@ -95,31 +145,50 @@ function PatternList( { filterValue, selectedCategory, patternCategories } ) { count ); debouncedSpeak( resultsFoundMessage ); - }, [ filterValue, debouncedSpeak, filteredBlockPatterns.length ] ); + }, [ searchValue, debouncedSpeak, filteredBlockPatterns.length ] ); - const currentShownPatterns = useAsyncList( filteredBlockPatterns, { - step: INITIAL_INSERTER_RESULTS, - } ); + const pagingProps = usePatternsPaging( + filteredBlockPatterns, + selectedCategory, + container, + patternSourceFilter + ); const hasItems = !! filteredBlockPatterns?.length; return ( -
- { hasItems && ( - - ) } +
+ + - { ! hasItems && } + { patternSourceFilter === PATTERN_TYPES.user && + ! searchValue && ( + + ) } + { hasItems && ( ) } + { pagingProps.numPages > 1 && ( + + ) }
); diff --git a/packages/block-editor/src/components/inserter/block-patterns-explorer/sidebar.js b/packages/block-editor/src/components/inserter/block-patterns-explorer/sidebar.js index 71431342221228..06ea794aa4daca 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-explorer/sidebar.js +++ b/packages/block-editor/src/components/inserter/block-patterns-explorer/sidebar.js @@ -4,6 +4,12 @@ import { Button, SearchControl } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import { default as BlockPatternsSourceFilter } from '../block-patterns-source-filter'; +import { allPatternsCategory } from '../block-patterns-tab'; + function PatternCategoriesList( { selectedCategory, patternCategories, @@ -31,14 +37,14 @@ function PatternCategoriesList( { ); } -function PatternsExplorerSearch( { filterValue, setFilterValue } ) { +function PatternsExplorerSearch( { searchValue, setSearchValue } ) { const baseClassName = 'block-editor-block-patterns-explorer__search'; return (
@@ -50,17 +56,26 @@ function PatternExplorerSidebar( { selectedCategory, patternCategories, onClickCategory, - filterValue, - setFilterValue, + patternSourceFilter, + setPatternSourceFilter, + searchValue, + setSearchValue, } ) { const baseClassName = 'block-editor-block-patterns-explorer__sidebar'; return (
+ { + setPatternSourceFilter( value ); + onClickCategory( allPatternsCategory.name ); + } } /> - { ! filterValue && ( + { ! searchValue && ( { + patternSourceFilters[ value ] = label; + return patternSourceFilters; + }, + {} +); + +export default function BlockPatternsSourceFilter( { onChange, value } ) { + return ( + + ); +} diff --git a/packages/block-editor/src/components/inserter/block-patterns-sync-filter.js b/packages/block-editor/src/components/inserter/block-patterns-sync-filter.js new file mode 100644 index 00000000000000..456c8bde270459 --- /dev/null +++ b/packages/block-editor/src/components/inserter/block-patterns-sync-filter.js @@ -0,0 +1,35 @@ +/** + * WordPress dependencies + */ +import { SelectControl } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +export const SYNC_TYPES = { + full: 'fully', + unsynced: 'unsynced', +}; + +const patternSyncOptions = [ + { value: 'all', label: __( 'All' ) }, + { value: SYNC_TYPES.full, label: __( 'Synced' ) }, + { value: SYNC_TYPES.unsynced, label: __( 'Standard' ) }, +]; + +export function BlockPatternsSyncFilter( { + setPatternSyncFilter, + patternSyncFilter, +} ) { + return ( + + ); +} diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab.js b/packages/block-editor/src/components/inserter/block-patterns-tab.js index f66d27ac06170d..8e29567343fee5 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-tab.js +++ b/packages/block-editor/src/components/inserter/block-patterns-tab.js @@ -8,8 +8,8 @@ import { useRef, useEffect, } from '@wordpress/element'; -import { _x, __, isRTL } from '@wordpress/i18n'; -import { useAsyncList, useViewportMatch } from '@wordpress/compose'; +import { _x, __, _n, isRTL, sprintf } from '@wordpress/i18n'; +import { useViewportMatch } from '@wordpress/compose'; import { __experimentalItemGroup as ItemGroup, __experimentalItem as Item, @@ -19,6 +19,7 @@ import { } from '@wordpress/components'; import { Icon, chevronRight, chevronLeft } from '@wordpress/icons'; import { focus } from '@wordpress/dom'; +import { speak } from '@wordpress/a11y'; /** * Internal dependencies @@ -27,29 +28,68 @@ import usePatternsState from './hooks/use-patterns-state'; import BlockPatternList from '../block-patterns-list'; import PatternsExplorerModal from './block-patterns-explorer/explorer'; import MobileTabNavigation from './mobile-tab-navigation'; +import BlockPatternsPaging from '../block-patterns-paging'; +import usePatternsPaging from './hooks/use-patterns-paging'; +import { + PATTERN_TYPES, + default as BlockPatternsSourceFilter, +} from './block-patterns-source-filter'; +import { + BlockPatternsSyncFilter, + SYNC_TYPES, +} from './block-patterns-sync-filter'; const noop = () => {}; -// Preferred order of pattern categories. Any other categories should -// be at the bottom without any re-ordering. -const patternCategoriesOrder = [ - 'custom', - 'featured', - 'posts', - 'text', - 'gallery', - 'call-to-action', - 'banner', - 'header', - 'footer', -]; +export const allPatternsCategory = { + name: 'allPatterns', + label: __( 'All categories' ), +}; + +export function isPatternFiltered( pattern, sourceFilter, syncFilter ) { + if ( + sourceFilter === PATTERN_TYPES.theme && + pattern.name.startsWith( 'core/block' ) + ) { + return true; + } + if ( sourceFilter === PATTERN_TYPES.user && ! pattern.id ) { + return true; + } + if ( + sourceFilter === PATTERN_TYPES.user && + syncFilter === SYNC_TYPES.full && + pattern.syncStatus !== '' + ) { + return true; + } + if ( + sourceFilter === PATTERN_TYPES.user && + syncFilter === SYNC_TYPES.unsynced && + pattern.syncStatus !== 'unsynced' + ) { + return true; + } + return false; +} -function usePatternsCategories( rootClientId ) { - const [ allPatterns, allCategories ] = usePatternsState( +export function usePatternsCategories( rootClientId, sourceFilter = 'all' ) { + const { patterns: allPatterns, allCategories } = usePatternsState( undefined, rootClientId ); + const filteredPatterns = useMemo( + () => + sourceFilter === 'all' + ? allPatterns + : allPatterns.filter( + ( pattern ) => + ! isPatternFiltered( pattern, sourceFilter ) + ), + [ sourceFilter, allPatterns ] + ); + const hasRegisteredCategory = useCallback( ( pattern ) => { if ( ! pattern.categories || ! pattern.categories.length ) { @@ -67,22 +107,14 @@ function usePatternsCategories( rootClientId ) { const populatedCategories = useMemo( () => { const categories = allCategories .filter( ( category ) => - allPatterns.some( ( pattern ) => + filteredPatterns.some( ( pattern ) => pattern.categories?.includes( category.name ) ) ) - .sort( ( { name: aName }, { name: bName } ) => { - // Sort categories according to `patternCategoriesOrder`. - let aIndex = patternCategoriesOrder.indexOf( aName ); - let bIndex = patternCategoriesOrder.indexOf( bName ); - // All other categories should come after that. - if ( aIndex < 0 ) aIndex = patternCategoriesOrder.length; - if ( bIndex < 0 ) bIndex = patternCategoriesOrder.length; - return aIndex - bIndex; - } ); + .sort( ( a, b ) => a.label.localeCompare( b.label ) ); if ( - allPatterns.some( + filteredPatterns.some( ( pattern ) => ! hasRegisteredCategory( pattern ) ) && ! categories.find( @@ -94,9 +126,25 @@ function usePatternsCategories( rootClientId ) { label: _x( 'Uncategorized' ), } ); } - + if ( filteredPatterns.length > 0 ) { + categories.unshift( { + name: allPatternsCategory.name, + label: allPatternsCategory.label, + } ); + } + speak( + sprintf( + /* translators: %d: number of categories . */ + _n( + '%d category button displayed.', + '%d category buttons displayed.', + categories.length + ), + categories.length + ) + ); return categories; - }, [ allCategories, allPatterns, hasRegisteredCategory ] ); + }, [ allCategories, filteredPatterns, hasRegisteredCategory ] ); return populatedCategories; } @@ -107,6 +155,7 @@ export function BlockPatternsCategoryDialog( { onHover, category, showTitlesAsTooltip, + patternFilter, } ) { const container = useRef(); @@ -129,6 +178,7 @@ export function BlockPatternsCategoryDialog( { onHover={ onHover } category={ category } showTitlesAsTooltip={ showTitlesAsTooltip } + patternFilter={ patternFilter } />
); @@ -140,16 +190,35 @@ export function BlockPatternsCategoryPanel( { onHover = noop, category, showTitlesAsTooltip, + patternFilter, } ) { - const [ allPatterns, , onClick ] = usePatternsState( + const { patterns: allPatterns, onClickPattern } = usePatternsState( onInsert, rootClientId ); + const [ patternSyncFilter, setPatternSyncFilter ] = useState( 'all' ); - const availableCategories = usePatternsCategories( rootClientId ); + const availableCategories = usePatternsCategories( + rootClientId, + patternFilter + ); + const container = useRef(); const currentCategoryPatterns = useMemo( () => allPatterns.filter( ( pattern ) => { + if ( + isPatternFiltered( + pattern, + patternFilter, + patternSyncFilter + ) + ) { + return false; + } + + if ( category.name === allPatternsCategory.name ) { + return true; + } if ( category.name !== 'uncategorized' ) { return pattern.categories?.includes( category.name ); } @@ -166,35 +235,60 @@ export function BlockPatternsCategoryPanel( { return availablePatternCategories.length === 0; } ), - [ allPatterns, availableCategories, category.name ] + [ + allPatterns, + availableCategories, + category.name, + patternFilter, + patternSyncFilter, + ] ); - const categoryPatternsList = useAsyncList( currentCategoryPatterns ); + const pagingProps = usePatternsPaging( + currentCategoryPatterns, + category, + container + ); // Hide block pattern preview on unmount. + // eslint-disable-next-line react-hooks/exhaustive-deps useEffect( () => () => onHover( null ), [] ); - if ( ! currentCategoryPatterns.length ) { - return null; - } - return ( -
+
{ category.label }

{ category.description }

- + { patternFilter === PATTERN_TYPES.user && ( + + ) } + { ! currentCategoryPatterns.length && ( +
{ __( 'No results found' ) }
+ ) } + { currentCategoryPatterns.length > 0 && ( + + ) } + { pagingProps.numPages > 1 && ( + + ) }
); } @@ -206,24 +300,40 @@ function BlockPatternsTabs( { rootClientId, } ) { const [ showPatternsExplorer, setShowPatternsExplorer ] = useState( false ); - const categories = usePatternsCategories( rootClientId ); + const [ patternSourceFilter, setPatternSourceFilter ] = useState( 'all' ); + + const categories = usePatternsCategories( + rootClientId, + patternSourceFilter + ); + const initialCategory = selectedCategory || categories[ 0 ]; const isMobile = useViewportMatch( 'medium', '<' ); return ( <> { ! isMobile && (
-