diff --git a/packages/block-editor/src/components/block-draggable/draggable-chip.js b/packages/block-editor/src/components/block-draggable/draggable-chip.js
index f5d8cf5eddc91..d7d053be179fa 100644
--- a/packages/block-editor/src/components/block-draggable/draggable-chip.js
+++ b/packages/block-editor/src/components/block-draggable/draggable-chip.js
@@ -1,7 +1,7 @@
/**
* WordPress dependencies
*/
-import { _n, sprintf } from '@wordpress/i18n';
+import { __, _n, sprintf } from '@wordpress/i18n';
import { Flex, FlexItem } from '@wordpress/components';
import { dragHandle } from '@wordpress/icons';
@@ -10,7 +10,8 @@ import { dragHandle } from '@wordpress/icons';
*/
import BlockIcon from '../block-icon';
-export default function BlockDraggableChip( { count, icon } ) {
+export default function BlockDraggableChip( { count, icon, isPattern } ) {
+ const patternLabel = isPattern && __( 'Pattern' );
return (
) : (
+ patternLabel ||
sprintf(
/* translators: %d: Number of blocks. */
_n( '%d block', '%d blocks', count ),
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 a77148155e6f3..925410e760f6a 100644
--- a/packages/block-editor/src/components/block-patterns-list/index.js
+++ b/packages/block-editor/src/components/block-patterns-list/index.js
@@ -22,14 +22,14 @@ function BlockPattern( { isDraggable, pattern, onClick, composite } ) {
const descriptionId = `block-editor-block-patterns-list__item-description-${ instanceId }`;
return (
-
+
{ ( { draggable, onDragStart, onDragEnd } ) => (
onClick( pattern, blocks ) }
+ aria-label={ pattern.title }
+ aria-describedby={
+ pattern.description ? descriptionId : undefined
+ }
>
{
+const InserterDraggableBlocks = ( {
+ isEnabled,
+ blocks,
+ icon,
+ children,
+ isPattern,
+} ) => {
const transferData = {
type: 'inserter',
blocks,
@@ -18,7 +24,11 @@ const InserterDraggableBlocks = ( { isEnabled, blocks, icon, children } ) => {
__experimentalTransferDataType="wp-blocks"
transferData={ transferData }
__experimentalDragComponent={
-
+
}
>
{ ( { onDraggableStart, onDraggableEnd } ) => {
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 997172432014f..fd96923114d0e 100644
--- a/packages/block-editor/src/components/inserter/block-patterns-tab.js
+++ b/packages/block-editor/src/components/inserter/block-patterns-tab.js
@@ -1,96 +1,37 @@
/**
* WordPress dependencies
*/
-import { useMemo, useState, useCallback } from '@wordpress/element';
-import { _x } from '@wordpress/i18n';
-import { useAsyncList } from '@wordpress/compose';
+import {
+ useMemo,
+ useState,
+ useCallback,
+ useRef,
+ useEffect,
+} from '@wordpress/element';
+import { _x, __, isRTL } from '@wordpress/i18n';
+import { useAsyncList, useViewportMatch } from '@wordpress/compose';
+import {
+ __experimentalItemGroup as ItemGroup,
+ __experimentalItem as Item,
+ __experimentalHStack as HStack,
+ __experimentalNavigatorProvider as NavigatorProvider,
+ __experimentalNavigatorScreen as NavigatorScreen,
+ __experimentalNavigatorButton as NavigatorButton,
+ __experimentalNavigatorBackButton as NavigatorBackButton,
+ FlexBlock,
+ Button,
+} from '@wordpress/components';
+import { Icon, chevronRight, chevronLeft } from '@wordpress/icons';
+import { focus } from '@wordpress/dom';
/**
* Internal dependencies
*/
-import PatternInserterPanel from './pattern-panel';
import usePatternsState from './hooks/use-patterns-state';
import BlockPatternList from '../block-patterns-list';
import PatternsExplorerModal from './block-patterns-explorer/explorer';
-function BlockPatternsCategory( {
- rootClientId,
- onInsert,
- selectedCategory,
- populatedCategories,
-} ) {
- const [ allPatterns, , onClick ] = usePatternsState(
- onInsert,
- rootClientId
- );
-
- const getPatternIndex = useCallback(
- ( pattern ) => {
- if ( ! pattern.categories?.length ) {
- return Infinity;
- }
- const indexedCategories = populatedCategories.reduce(
- ( accumulator, { name }, index ) => {
- accumulator[ name ] = index;
- return accumulator;
- },
- {}
- );
- return Math.min(
- ...pattern.categories.map( ( cat ) =>
- indexedCategories[ cat ] !== undefined
- ? indexedCategories[ cat ]
- : Infinity
- )
- );
- },
- [ populatedCategories ]
- );
-
- const currentCategoryPatterns = useMemo(
- () =>
- allPatterns.filter( ( pattern ) =>
- selectedCategory.name === 'uncategorized'
- ? getPatternIndex( pattern ) === Infinity
- : pattern.categories?.includes( selectedCategory.name )
- ),
- [ allPatterns, selectedCategory ]
- );
-
- // Ordering the patterns is important for the async rendering.
- const orderedPatterns = useMemo( () => {
- return currentCategoryPatterns.sort( ( a, b ) => {
- return getPatternIndex( a ) - getPatternIndex( b );
- } );
- }, [ currentCategoryPatterns, getPatternIndex ] );
-
- const currentShownPatterns = useAsyncList( orderedPatterns );
-
- if ( ! currentCategoryPatterns.length ) {
- return null;
- }
-
- return (
-
-
-
- );
-}
-
-function BlockPatternsTabs( {
- rootClientId,
- onInsert,
- onClickCategory,
- selectedCategory,
-} ) {
- const [ showPatternsExplorer, setShowPatternsExplorer ] = useState( false );
+function usePatternsCategories() {
const [ allPatterns, allCategories ] = usePatternsState();
const hasRegisteredCategory = useCallback(
@@ -138,30 +79,173 @@ function BlockPatternsTabs( {
return categories;
}, [ allPatterns, allCategories ] );
- const patternCategory = selectedCategory
- ? selectedCategory
- : populatedCategories[ 0 ];
+ return populatedCategories;
+}
+
+export function BlockPatternsCategoryDialog( {
+ rootClientId,
+ onInsert,
+ category,
+} ) {
+ const container = useRef();
+
+ useEffect( () => {
+ const timeout = setTimeout( () => {
+ const [ firstTabbable ] = focus.tabbable.find( container.current );
+ firstTabbable?.focus();
+ } );
+ return () => clearTimeout( timeout );
+ }, [ category ] );
return (
- <>
- setShowPatternsExplorer( true ) }
+
+
- { ! showPatternsExplorer && (
-
+ );
+}
+
+export function BlockPatternsCategoryPanel( {
+ rootClientId,
+ onInsert,
+ category,
+} ) {
+ const [ allPatterns, , onClick ] = usePatternsState(
+ onInsert,
+ rootClientId
+ );
+
+ const availableCategories = usePatternsCategories();
+ const currentCategoryPatterns = useMemo(
+ () =>
+ allPatterns.filter( ( pattern ) => {
+ if ( category.name !== 'uncategorized' ) {
+ return pattern.categories?.includes( category.name );
+ }
+
+ // The uncategorized category should show all the patterns without any category
+ // or with no available category.
+ const availablePatternCategories =
+ pattern.categories?.filter( ( cat ) =>
+ availableCategories.find(
+ ( availableCategory ) =>
+ availableCategory.name === cat
+ )
+ ) ?? [];
+
+ return availablePatternCategories.length === 0;
+ } ),
+ [ allPatterns, category ]
+ );
+
+ const currentShownPatterns = useAsyncList( currentCategoryPatterns );
+
+ if ( ! currentCategoryPatterns.length ) {
+ return null;
+ }
+
+ return (
+
+
+ { category.label }
+
+
{ category.description }
+
+
+ );
+}
+
+function BlockPatternsTabs( {
+ onSelectCategory,
+ selectedCategory,
+ onInsert,
+ rootClientId,
+} ) {
+ const [ showPatternsExplorer, setShowPatternsExplorer ] = useState( false );
+ const categories = usePatternsCategories();
+ const isMobile = useViewportMatch( 'medium', '<' );
+
+ return (
+ <>
+ { ! isMobile && (
+
+
+
+ ) }
+ { isMobile && (
+
) }
{ showPatternsExplorer && (
setShowPatternsExplorer( false ) }
/>
) }
@@ -169,4 +253,54 @@ function BlockPatternsTabs( {
);
}
+function BlockPatternsTabNavigation( { onInsert, rootClientId } ) {
+ const categories = usePatternsCategories();
+
+ return (
+
+
+
+ { categories.map( ( category ) => (
+
+
+ { category.label }
+
+
+
+ ) ) }
+
+
+
+ { categories.map( ( category ) => (
+
+
+ { __( 'Back' ) }
+
+
+
+ ) ) }
+
+ );
+}
+
export default BlockPatternsTabs;
diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js
index 5a10d07dd1caa..c6d019c8ff9a7 100644
--- a/packages/block-editor/src/components/inserter/menu.js
+++ b/packages/block-editor/src/components/inserter/menu.js
@@ -24,7 +24,9 @@ import { useSelect } from '@wordpress/data';
import Tips from './tips';
import InserterPreviewPanel from './preview-panel';
import BlockTypesTab from './block-types-tab';
-import BlockPatternsTabs from './block-patterns-tab';
+import BlockPatternsTabs, {
+ BlockPatternsCategoryDialog,
+} from './block-patterns-tab';
import ReusableBlocksTab from './reusable-blocks-tab';
import InserterSearchResults from './search-results';
import useInsertionPoint from './hooks/use-insertion-point';
@@ -52,6 +54,7 @@ function InserterMenu(
const [ hoveredItem, setHoveredItem ] = useState( null );
const [ selectedPatternCategory, setSelectedPatternCategory ] =
useState( null );
+ const [ selectedTab, setSelectedTab ] = useState( null );
const [ destinationRootClientId, onInsertBlocks, onToggleInsertionPoint ] =
useInsertionPoint( {
@@ -144,7 +147,7 @@ function InserterMenu(
),
@@ -186,6 +189,8 @@ function InserterMenu(
},
} ) );
+ const showPatternPanel =
+ selectedTab === 'patterns' && ! filterValue && selectedPatternCategory;
const showAsTabs = ! filterValue && ( showPatterns || hasReusableBlocks );
return (
@@ -228,6 +233,7 @@ function InserterMenu(
showPatterns={ showPatterns }
showReusableBlocks={ hasReusableBlocks }
prioritizePatterns={ prioritizePatterns }
+ onSelect={ setSelectedTab }
>
{ getCurrentTab }
@@ -241,6 +247,13 @@ function InserterMenu(
{ showInserterHelpPanel && hoveredItem && (
) }
+ { showPatternPanel && (
+
+ ) }
);
}
diff --git a/packages/block-editor/src/components/inserter/pattern-panel.js b/packages/block-editor/src/components/inserter/pattern-panel.js
deleted file mode 100644
index 025c11cb2ce58..0000000000000
--- a/packages/block-editor/src/components/inserter/pattern-panel.js
+++ /dev/null
@@ -1,93 +0,0 @@
-/**
- * External dependencies
- */
-import classnames from 'classnames';
-
-/**
- * WordPress dependencies
- */
-import { Flex, FlexItem, SelectControl, Button } from '@wordpress/components';
-import { __, _x } from '@wordpress/i18n';
-import { useViewportMatch } from '@wordpress/compose';
-
-function PatternInserterPanel( {
- selectedCategory,
- patternCategories,
- onClickCategory,
- openPatternExplorer,
-} ) {
- const isMobile = useViewportMatch( 'medium', '<' );
- const categoryOptions = () => {
- const options = [];
-
- patternCategories.map( ( patternCategory ) => {
- return options.push( {
- value: patternCategory.name,
- label: patternCategory.label,
- } );
- } );
-
- return options;
- };
-
- const onChangeSelect = ( selected ) => {
- onClickCategory(
- patternCategories.find(
- ( patternCategory ) => selected === patternCategory.name
- )
- );
- };
-
- const className = classnames(
- 'block-editor-inserter__panel-header',
- 'block-editor-inserter__panel-header-patterns'
- );
-
- // In iOS-based mobile devices, the onBlur will fire when selecting an option
- // from a Select element. To prevent closing the useDialog on iOS devices, we
- // stop propagating the onBlur event if there is no relatedTarget, which means
- // that the user most likely did not click on an element within the editor canvas.
- const onBlur = ( event ) => {
- if ( ! event?.relatedTarget ) {
- event.stopPropagation();
- }
- };
-
- return (
-
-
-
-
- { ! isMobile && (
-
-
-
- ) }
-
- );
-}
-
-export default PatternInserterPanel;
diff --git a/packages/block-editor/src/components/inserter/style.scss b/packages/block-editor/src/components/inserter/style.scss
index 32e5093233209..210f2db211c00 100644
--- a/packages/block-editor/src/components/inserter/style.scss
+++ b/packages/block-editor/src/components/inserter/style.scss
@@ -22,14 +22,13 @@ $block-inserter-tabs-height: 44px;
flex-direction: column;
height: 100%;
gap: $grid-unit-20;
- width: auto;
- @include break-medium {
- width: $block-inserter-width;
- }
-
&.show-as-tabs {
gap: 0;
}
+
+ @include break-medium {
+ width: $block-inserter-width;
+ }
}
.block-editor-inserter__popover.is-quick {
@@ -148,10 +147,6 @@ $block-inserter-tabs-height: 44px;
padding: $grid-unit-20 $grid-unit-20 0;
}
-.block-editor-inserter__panel-header-patterns {
- padding: $grid-unit-20 $grid-unit-20 0;
-}
-
.block-editor-inserter__panel-content {
padding: $grid-unit-20;
}
@@ -235,6 +230,92 @@ $block-inserter-tabs-height: 44px;
}
}
+.block-editor-inserter__patterns-explore-button.components-button {
+ padding: $grid-unit-20;
+ justify-content: center;
+ margin-top: $grid-unit-20;
+ width: 100%;
+}
+
+.block-editor-inserter__patterns-selected-category.block-editor-inserter__patterns-selected-category {
+ color: var(--wp-admin-theme-color);
+ position: relative;
+
+ .components-flex-item {
+ filter: brightness(0.95);
+ }
+
+ svg {
+ fill: var(--wp-admin-theme-color);
+ }
+
+ &::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ border-radius: $radius-block-ui;
+ opacity: 0.04;
+ background: var(--wp-admin-theme-color);
+ }
+}
+
+.block-editor-inserter__block-patterns-tabs-container {
+ height: 100%;
+ nav {
+ height: 100%;
+ }
+}
+
+.block-editor-inserter__block-patterns-tabs {
+ display: flex;
+ flex-direction: column;
+ padding: $grid-unit-20;
+ overflow-y: auto;
+ height: 100%;
+}
+
+.block-editor-inserter__patterns-fill-space {
+ flex-grow: 1;
+}
+
+.block-editor-inserter__patterns-category-panel {
+ background: $gray-100;
+ border-left: $border-width solid $gray-200;
+ border-right: $border-width solid $gray-200;
+ position: absolute;
+ padding: $grid-unit-40 $grid-unit-30;
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 100%;
+ overflow-y: auto;
+ scrollbar-gutter: stable both-edges;
+
+ @include break-medium {
+ left: 100%;
+ display: block;
+ width: 300px;
+ }
+
+ .block-editor-block-patterns-list {
+ margin-top: $grid-unit-30;
+ }
+
+ .block-editor-block-preview__container {
+ box-shadow: 0 15px 25px rgb(0 0 0 / 7%);
+ &:hover {
+ box-shadow: 0 0 0 2px $gray-900, 0 15px 25px rgb(0 0 0 / 7%);
+ }
+ }
+
+ .block-editor-block-patterns-list__item-title {
+ display: none;
+ }
+}
+
.block-editor-inserter__preview-content {
min-height: $grid-unit-60 * 3;
background: $gray-100;
@@ -388,3 +469,7 @@ $block-inserter-tabs-height: 44px;
}
}
}
+
+.block-editor-inserter__patterns-category-panel-title {
+ font-size: calc(1.25 * 13px);
+}
diff --git a/packages/components/src/tab-panel/index.tsx b/packages/components/src/tab-panel/index.tsx
index b34ccff5f01c2..8388d715b2343 100644
--- a/packages/components/src/tab-panel/index.tsx
+++ b/packages/components/src/tab-panel/index.tsx
@@ -85,7 +85,7 @@ export function TabPanel( {
const instanceId = useInstanceId( TabPanel, 'tab-panel' );
const [ selected, setSelected ] = useState< string >();
- const handleClick = ( tabKey: string ) => {
+ const handleTabSelection = ( tabKey: string ) => {
setSelected( tabKey );
onSelect?.( tabKey );
};
@@ -98,11 +98,8 @@ export function TabPanel( {
useEffect( () => {
const newSelectedTab = find( tabs, { name: selected } );
- if ( ! newSelectedTab ) {
- setSelected(
- initialTabName ||
- ( tabs.length > 0 ? tabs[ 0 ].name : undefined )
- );
+ if ( ! newSelectedTab && tabs.length > 0 ) {
+ handleTabSelection( initialTabName || tabs[ 0 ].name );
}
}, [ tabs ] );
@@ -127,7 +124,7 @@ export function TabPanel( {
aria-controls={ `${ instanceId }-${ tab.name }-view` }
selected={ tab.name === selected }
key={ tab.name }
- onClick={ () => handleClick( tab.name ) }
+ onClick={ () => handleTabSelection( tab.name ) }
>
{ tab.title }