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

Suggest Block patterns in block placeholder states #29602

Merged
merged 15 commits into from
Apr 22, 2021
5 changes: 5 additions & 0 deletions packages/base-styles/_z-index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ $z-layers: (
// should overlap most block content.
".block-editor-block-list__block.is-{selected,hovered} .block-editor-block-mover": 61,

// Query block setup state.
".block-editor-block-pattern-setup .pattern-slide": 100,
".block-editor-block-pattern-setup .{next,previous}-slide": 101,
".block-editor-block-pattern-setup .active-slide": 102,

// Show sidebar above wp-admin navigation bar for mobile viewports:
// #wpadminbar { z-index: 99999 }
".interface-interface-skeleton__sidebar": 100000,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const VIEWMODES = {
carousel: 'carousel',
grid: 'grid',
};
184 changes: 184 additions & 0 deletions packages/block-editor/src/components/block-pattern-setup/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/**
* WordPress dependencies
*/
import { useDispatch } from '@wordpress/data';
import { cloneBlock } from '@wordpress/blocks';
import {
VisuallyHidden,
__unstableComposite as Composite,
__unstableUseCompositeState as useCompositeState,
__unstableCompositeItem as CompositeItem,
} from '@wordpress/components';

import { useState } from '@wordpress/element';
import { useInstanceId } from '@wordpress/compose';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import { store as blockEditorStore } from '../../store';
import BlockPreview from '../block-preview';
import SetupToolbar from './setup-toolbar';
import usePatternsSetup from './use-patterns-setup';
import { VIEWMODES } from './constants';

const SetupContent = ( {
viewMode,
activeSlide,
patterns,
onBlockPatternSelect,
} ) => {
const composite = useCompositeState();
const containerClass = 'block-editor-block-pattern-setup__container';
if ( viewMode === VIEWMODES.carousel ) {
const slideClass = new Map( [
[ activeSlide, 'active-slide' ],
[ activeSlide - 1, 'previous-slide' ],
[ activeSlide + 1, 'next-slide' ],
] );
return (
<div className={ containerClass }>
<ul className="carousel-container">
{ patterns.map( ( pattern, index ) => (
<BlockPatternSlide
className={ slideClass.get( index ) || '' }
key={ pattern.name }
pattern={ pattern }
/>
) ) }
</ul>
</div>
);
}
return (
<Composite
{ ...composite }
role="listbox"
className={ containerClass }
aria-label={ __( 'Patterns list' ) }
>
{ patterns.map( ( pattern ) => (
<BlockPattern
key={ pattern.name }
pattern={ pattern }
onSelect={ onBlockPatternSelect }
composite={ composite }
/>
) ) }
</Composite>
);
};

function BlockPattern( { pattern, onSelect, composite } ) {
const baseClassName = 'block-editor-block-pattern-setup-list';
const { blocks, title, description, viewportWidth = 700 } = pattern;
const descriptionId = useInstanceId(
BlockPattern,
`${ baseClassName }__item-description`
);
return (
<div
className={ `${ baseClassName }__list-item` }
aria-label={ pattern.title }
aria-describedby={ pattern.description ? descriptionId : undefined }
>
<CompositeItem
role="option"
as="div"
ntsekouras marked this conversation as resolved.
Show resolved Hide resolved
{ ...composite }
className={ `${ baseClassName }__item` }
onClick={ () => onSelect( blocks ) }
>
<BlockPreview
blocks={ blocks }
viewportWidth={ viewportWidth }
/>
<div className={ `${ baseClassName }__item-title` }>
{ title }
</div>
</CompositeItem>
{ !! description && (
<VisuallyHidden id={ descriptionId }>
{ description }
</VisuallyHidden>
) }
</div>
);
}

function BlockPatternSlide( { className, pattern } ) {
ntsekouras marked this conversation as resolved.
Show resolved Hide resolved
const { blocks, title, description } = pattern;
const descriptionId = useInstanceId(
BlockPatternSlide,
'block-editor-block-pattern-setup-list__item-description'
);
return (
<li
className={ `pattern-slide ${ className }` }
aria-label={ title }
aria-describedby={ description ? descriptionId : undefined }
>
<BlockPreview blocks={ blocks } __experimentalLive />
{ !! description && (
<VisuallyHidden id={ descriptionId }>
{ description }
</VisuallyHidden>
) }
</li>
);
}

const BlockPatternSetup = ( {
clientId,
blockName,
filterPatternsFn,
startBlankComponent,
} ) => {
const [ viewMode, setViewMode ] = useState( VIEWMODES.carousel );
const [ activeSlide, setActiveSlide ] = useState( 0 );
const [ showBlank, setShowBlank ] = useState( false );
const { replaceBlock } = useDispatch( blockEditorStore );
const patterns = usePatternsSetup( clientId, blockName, filterPatternsFn );

if ( ! patterns?.length || showBlank ) {
return startBlankComponent;
}

const onBlockPatternSelect = ( blocks ) => {
const clonedBlocks = blocks.map( ( block ) => cloneBlock( block ) );
replaceBlock( clientId, clonedBlocks );
};
return (
<div
className={ `block-editor-block-pattern-setup view-mode-${ viewMode }` }
>
<SetupToolbar
viewMode={ viewMode }
setViewMode={ setViewMode }
activeSlide={ activeSlide }
totalSlides={ patterns.length }
handleNext={ () => {
setActiveSlide( ( active ) => active + 1 );
} }
handlePrevious={ () => {
setActiveSlide( ( active ) => active - 1 );
} }
onBlockPatternSelect={ () => {
onBlockPatternSelect( patterns[ activeSlide ].blocks );
} }
onStartBlank={ () => {
setShowBlank( true );
} }
/>
<SetupContent
viewMode={ viewMode }
activeSlide={ activeSlide }
patterns={ patterns }
onBlockPatternSelect={ onBlockPatternSelect }
/>
</div>
);
};

export default BlockPatternSetup;
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import {
chevronRight,
chevronLeft,
grid,
stretchFullWidth,
} from '@wordpress/icons';

/**
* Internal dependencies
*/
import { VIEWMODES } from './constants';

const Actions = ( { onStartBlank, onBlockPatternSelect } ) => (
<div className="block-editor-block-pattern-setup__actions">
<Button onClick={ onStartBlank }>{ __( 'Start blank' ) }</Button>
<Button isPrimary onClick={ onBlockPatternSelect }>
{ __( 'Choose' ) }
</Button>
</div>
);

const CarouselNavigation = ( {
handlePrevious,
handleNext,
activeSlide,
totalSlides,
} ) => (
<div className="block-editor-block-pattern-setup__navigation">
<Button
icon={ chevronLeft }
label={ __( 'Previous pattern' ) }
onClick={ handlePrevious }
disabled={ activeSlide === 0 }
/>
<Button
icon={ chevronRight }
label={ __( 'Next pattern' ) }
onClick={ handleNext }
disabled={ activeSlide === totalSlides - 1 }
/>
</div>
);

const SetupToolbar = ( {
viewMode,
setViewMode,
handlePrevious,
handleNext,
activeSlide,
totalSlides,
onBlockPatternSelect,
onStartBlank,
} ) => {
const isCarouselView = viewMode === VIEWMODES.carousel;
const displayControls = (
<div className="block-editor-block-pattern-setup__display-controls">
<Button
icon={ stretchFullWidth }
label={ __( 'Carousel view' ) }
onClick={ () => setViewMode( VIEWMODES.carousel ) }
isPressed={ isCarouselView }
/>
<Button
icon={ grid }
label={ __( 'Grid view' ) }
onClick={ () => setViewMode( VIEWMODES.grid ) }
isPressed={ viewMode === VIEWMODES.grid }
/>
</div>
);
return (
<div className="block-editor-block-pattern-setup__toolbar">
{ isCarouselView && (
<CarouselNavigation
handlePrevious={ handlePrevious }
handleNext={ handleNext }
activeSlide={ activeSlide }
totalSlides={ totalSlides }
/>
) }
{ displayControls }
{ isCarouselView && (
<Actions
onBlockPatternSelect={ onBlockPatternSelect }
onStartBlank={ onStartBlank }
/>
) }
</div>
);
};

export default SetupToolbar;
Loading