From bf4eddb29f4c23ce841076b63035350866e81bfe Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 18 Oct 2022 09:00:36 +0100 Subject: [PATCH] Redesign the main pattern inserter (#44028) --- .../block-draggable/draggable-chip.js | 6 +- .../components/block-patterns-list/index.js | 14 +- .../components/block-patterns-list/style.scss | 5 +- .../inserter-draggable-blocks/index.js | 14 +- .../components/inserter/block-patterns-tab.js | 330 ++++++++++++------ .../src/components/inserter/menu.js | 17 +- .../src/components/inserter/pattern-panel.js | 93 ----- .../src/components/inserter/style.scss | 103 +++++- packages/components/src/tab-panel/index.tsx | 11 +- 9 files changed, 372 insertions(+), 221 deletions(-) delete mode 100644 packages/block-editor/src/components/inserter/pattern-panel.js 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 f5d8cf5eddc91e..d7d053be179fa6 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 a77148155e6f32..925410e760f6a4 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 997172432014fa..fd96923114d0e2 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 5a10d07dd1caad..c6d019c8ff9a74 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 025c11cb2ce589..00000000000000 --- 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 32e50932332092..210f2db211c00a 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 b34ccff5f01c2b..8388d715b23431 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 }