Customize your store header (#40107)

This closes #39718 by loading header patterns in the header customization menu of the assembler.
This commit is contained in:
Sam Seay 2023-09-14 16:24:46 +08:00 committed by GitHub
parent 473a53d542
commit 36c644a1c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 806 additions and 509 deletions

View File

@ -4,23 +4,21 @@
* External dependencies
*/
import classNames from 'classnames';
import { useSelect } from '@wordpress/data';
// @ts-ignore No types for this exist yet.
import { useEntityRecords, useEntityBlockEditor } from '@wordpress/core-data';
import { useEntityRecords } from '@wordpress/core-data';
// @ts-ignore No types for this exist yet.
import { privateApis as routerPrivateApis } from '@wordpress/router';
// @ts-ignore No types for this exist yet.
import { store as editSiteStore } from '@wordpress/edit-site/build-module/store';
// @ts-ignore No types for this exist yet.
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
// @ts-ignore No types for this exist yet.
import useSiteEditorSettings from '@wordpress/edit-site/build-module/components/block-editor/use-site-editor-settings';
import { BlockInstance } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import BlockPreview from './block-preview';
import { useCallback } from '@wordpress/element';
import { useEditorBlocks } from './hooks/use-editor-blocks';
const { useHistory, useLocation } = unlock( routerPrivateApis );
@ -38,19 +36,7 @@ export const BlockEditor = ( {} ) => {
const history = useHistory();
const location = useLocation();
const settings = useSiteEditorSettings();
const { templateType } = useSelect( ( select ) => {
const { getEditedPostType } = unlock( select( editSiteStore ) );
return {
templateType: getEditedPostType(),
};
}, [] );
const [ blocks ]: [ BlockInstance[] ] = useEntityBlockEditor(
'postType',
templateType
);
const [ blocks ] = useEditorBlocks();
// // See packages/block-library/src/page-list/edit.js.
const { records: pages } = useEntityRecords( 'postType', 'page', {
@ -145,6 +131,7 @@ export const BlockEditor = ( {} ) => {
</div>
);
}
return (
<div className="woocommerce-customize-store__block-editor">
<div className={ 'woocommerce-block-preview-container' }>

View File

@ -7,7 +7,7 @@
*/
// @ts-ignore No types for this exist yet.
import { BlockEditorProvider } from '@wordpress/block-editor';
import { memo, useMemo } from '@wordpress/element';
import { memo, useContext, useMemo } from '@wordpress/element';
import { BlockInstance } from '@wordpress/blocks';
/**
* Internal dependencies
@ -16,11 +16,14 @@ import {
AutoHeightBlockPreview,
ScaledBlockPreviewProps,
} from './auto-block-preview';
import { HighlightedBlockContext } from './context/highlighted-block-context';
import { useScrollOpacity } from './hooks/use-scroll-opacity';
export const BlockPreview = ( {
blocks,
settings,
useSubRegistry = true,
additionalStyles,
...props
}: {
blocks: BlockInstance | BlockInstance[];
@ -32,13 +35,45 @@ export const BlockPreview = ( {
[ blocks ]
);
const { highlightedBlockIndex } = useContext( HighlightedBlockContext );
const previewOpacity = useScrollOpacity(
'.interface-navigable-region.interface-interface-skeleton__content',
'topDown'
);
const opacityStyles =
highlightedBlockIndex === -1
? ''
: `
.wp-block.preview-opacity {
opacity: ${ previewOpacity };
}
`;
return (
<BlockEditorProvider
value={ renderedBlocks }
value={ renderedBlocks.map( ( block, i ) => {
if ( i === highlightedBlockIndex ) {
return block;
}
return {
...block,
attributes: {
...block.attributes,
className:
block.attributes.className + ' preview-opacity',
},
};
} ) }
settings={ settings }
useSubRegistry={ useSubRegistry }
>
<AutoHeightBlockPreview settings={ settings } { ...props } />
<AutoHeightBlockPreview
settings={ settings }
additionalStyles={ `${ opacityStyles } ${ additionalStyles }` }
{ ...props }
/>
</BlockEditorProvider>
);
};

View File

@ -0,0 +1,43 @@
/**
* External dependencies
*/
import React, { createContext, useState } from '@wordpress/element';
import type { ReactNode } from 'react';
export const HighlightedBlockContext = createContext( {
highlightedBlockIndex: -1,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
setHighlightedBlockIndex: ( index: number ) => {
// No op by default.
},
resetHighlightedBlockIndex: () => {
// No op by default.
},
} );
// A Provider that keeps track of which block is "focussed" in the Assembler Hub.
// This is used to highlight the block in the BlockEditor currently.
export const HighlightedBlockContextProvider = ( {
children,
}: {
children: ReactNode;
} ) => {
// Create some state
const [ highlightedBlockIndex, setHighlightedBlockIndex ] = useState( -1 );
const resetHighlightedBlockIndex = () => {
setHighlightedBlockIndex( -1 );
};
return (
<HighlightedBlockContext.Provider
value={ {
highlightedBlockIndex,
setHighlightedBlockIndex,
resetHighlightedBlockIndex,
} }
>
{ children }
</HighlightedBlockContext.Provider>
);
};

View File

@ -7,8 +7,6 @@
import classnames from 'classnames';
import { useMemo } from '@wordpress/element';
// @ts-ignore No types for this exist yet.
import { EntityProvider } from '@wordpress/core-data';
// @ts-ignore No types for this exist yet.
import { InterfaceSkeleton } from '@wordpress/interface';
import { useSelect, useDispatch } from '@wordpress/data';
// @ts-ignore No types for this exist yet.
@ -18,8 +16,6 @@ import { store as editSiteStore } from '@wordpress/edit-site/build-module/store'
// @ts-ignore No types for this exist yet.
import CanvasSpinner from '@wordpress/edit-site/build-module/components/canvas-spinner';
// @ts-ignore No types for this exist yet.
import useEditedEntityRecord from '@wordpress/edit-site/build-module/components/use-edited-entity-record';
// @ts-ignore No types for this exist yet.
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
// @ts-ignore No types for this exist yet.
import { GlobalStylesRenderer } from '@wordpress/edit-site/build-module/components/global-styles-renderer';
@ -30,8 +26,6 @@ import { GlobalStylesRenderer } from '@wordpress/edit-site/build-module/componen
import { BlockEditor } from './block-editor';
export const Editor = ( { isLoading }: { isLoading: boolean } ) => {
const { record: template } = useEditedEntityRecord();
const { id: templateId, type: templateType } = template;
const { context, hasPageContentFocus } = useSelect( ( select ) => {
const {
getEditedPostContext,
@ -68,33 +62,26 @@ export const Editor = ( { isLoading }: { isLoading: boolean } ) => {
return (
<>
{ isLoading ? <CanvasSpinner /> : null }
<EntityProvider kind="root" type="site">
<EntityProvider
kind="postType"
type={ templateType }
id={ templateId }
>
<BlockContextProvider value={ blockContext }>
<InterfaceSkeleton
enableRegionNavigation={ false }
className={ classnames(
'woocommerce-customize-store__edit-site-editor',
'edit-site-editor__interface-skeleton',
{
'show-icon-labels': false,
'is-loading': isLoading,
}
) }
content={
<>
<GlobalStylesRenderer />
<BlockEditor />
</>
}
/>
</BlockContextProvider>
</EntityProvider>
</EntityProvider>
<BlockContextProvider value={ blockContext }>
<InterfaceSkeleton
enableRegionNavigation={ false }
className={ classnames(
'woocommerce-customize-store__edit-site-editor',
'edit-site-editor__interface-skeleton',
{
'show-icon-labels': false,
'is-loading': isLoading,
}
) }
content={
<>
<GlobalStylesRenderer />
<BlockEditor />
</>
}
/>
</BlockContextProvider>
</>
);
};

View File

@ -0,0 +1,43 @@
/* eslint-disable @woocommerce/dependency-group */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/**
* External dependencies
*/
// @ts-ignore No types for this exist yet.
import { useEntityBlockEditor } from '@wordpress/core-data';
// @ts-ignore No types for this exist yet.
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
// @ts-ignore No types for this exist yet.
import { store as editSiteStore } from '@wordpress/edit-site/build-module/store';
import { useSelect } from '@wordpress/data';
import { BlockInstance } from '@wordpress/blocks';
type ChangeHandler = (
blocks: BlockInstance[],
options: Record< string, unknown >
) => void;
// Note, must be used within BlockEditorProvider. This allows shared access of blocks currently
// being edited in the BlockEditor.
export const useEditorBlocks = (): [
BlockInstance[],
ChangeHandler,
ChangeHandler
] => {
const { templateType } = useSelect( ( select ) => {
const { getEditedPostType } = unlock( select( editSiteStore ) );
return {
templateType: getEditedPostType(),
};
}, [] );
// @ts-ignore Types are not up to date.
const [ blocks, onInput, onChange ]: [
BlockInstance[],
ChangeHandler,
ChangeHandler
] = useEntityBlockEditor( 'postType', templateType );
return [ blocks, onInput, onChange ];
};

View File

@ -0,0 +1,56 @@
/* eslint-disable @woocommerce/dependency-group */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/**
* External dependencies
*/
import { useSelect } from '@wordpress/data';
// @ts-ignore No types for this exist yet.
import { store as coreStore } from '@wordpress/core-data';
import { useMemo } from '@wordpress/element';
import { BlockInstance, parse } from '@wordpress/blocks';
type Pattern = {
blockTypes: string[];
categories: string[];
content: string;
name: string;
source: string;
title: string;
};
type PatternWithBlocks = Pattern & {
blocks: BlockInstance[];
};
export const usePatternsByCategory = ( category: string ) => {
const { blockPatterns, isLoading } = useSelect(
( select ) => ( {
// @ts-ignore - This is valid.
blockPatterns: select( coreStore ).getBlockPatterns(),
isLoading:
// @ts-ignore - This is valid.
! select( coreStore ).hasFinishedResolution(
'getBlockPatterns'
),
} ),
[]
);
const patternsByCategory: PatternWithBlocks[] = useMemo( () => {
return ( blockPatterns || [] )
.filter( ( pattern: Pattern ) =>
pattern.categories?.includes( category )
)
.map( ( pattern: Pattern ) => {
return {
...pattern,
// @ts-ignore - Passing options is valid, but not in the type.
blocks: parse( pattern.content, {
__unstableSkipMigrationLogs: true,
} ),
};
} );
}, [ blockPatterns, category ] );
return { isLoading, patterns: patternsByCategory };
};

View File

@ -0,0 +1,57 @@
/**
* External dependencies
*/
import { useEffect, useState } from '@wordpress/element';
type ScrollDirection = 'topDown' | 'bottomUp';
export const useScrollOpacity = (
selector: string,
direction: ScrollDirection = 'topDown',
sensitivity = 0.2
) => {
const [ opacity, setOpacity ] = useState( 0.05 );
useEffect( () => {
const targetElement = document.querySelector( selector );
const handleScroll = () => {
if ( targetElement ) {
const maxScrollHeight =
targetElement.scrollHeight - targetElement.clientHeight;
const currentScrollPosition = targetElement.scrollTop;
const maxEffectScroll = maxScrollHeight * sensitivity;
let calculatedOpacity;
if ( direction === 'bottomUp' ) {
calculatedOpacity =
1 - currentScrollPosition / maxEffectScroll;
} else {
calculatedOpacity = currentScrollPosition / maxEffectScroll;
}
calculatedOpacity = 0.1 + 0.9 * calculatedOpacity;
// Clamp opacity between 0.1 and 1
calculatedOpacity = Math.max(
0.1,
Math.min( calculatedOpacity, 1 )
);
setOpacity( calculatedOpacity );
}
};
if ( targetElement ) {
targetElement.addEventListener( 'scroll', handleScroll );
}
return () => {
if ( targetElement ) {
targetElement.removeEventListener( 'scroll', handleScroll );
}
};
}, [ selector, direction, sensitivity ] );
return opacity;
};

View File

@ -30,6 +30,10 @@ import ErrorBoundary from '@wordpress/edit-site/build-module/components/error-bo
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
// @ts-ignore No types for this exist yet.
import { NavigableRegion } from '@wordpress/interface';
// @ts-ignore No types for this exist yet.
import { EntityProvider } from '@wordpress/core-data';
// @ts-ignore No types for this exist yet.
import useEditedEntityRecord from '@wordpress/edit-site/build-module/components/use-edited-entity-record';
/**
* Internal dependencies
@ -40,6 +44,7 @@ import { SiteHub } from './site-hub';
import { LogoBlockContext } from './logo-block-context';
import ResizableFrame from './resizable-frame';
import { OnboardingTour, useOnboardingTour } from './onboarding-tour';
import { HighlightedBlockContextProvider } from './context/highlighted-block-context';
const { useGlobalStyle } = unlock( blockEditorPrivateApis );
@ -66,6 +71,9 @@ export const Layout = () => {
const [ backgroundColor ] = useGlobalStyle( 'color.background' );
const [ gradientValue ] = useGlobalStyle( 'color.gradient' );
const { record: template } = useEditedEntityRecord();
const { id: templateId, type: templateType } = template;
return (
<LogoBlockContext.Provider
value={ {
@ -73,112 +81,132 @@ export const Layout = () => {
setLogoBlock,
} }
>
<div className={ classnames( 'edit-site-layout' ) }>
<motion.div
className="edit-site-layout__header-container"
animate={ 'view' }
>
<SiteHub
as={ motion.div }
variants={ {
view: { x: 0 },
} }
isTransparent={ false }
className="edit-site-layout__hub"
/>
</motion.div>
<div className="edit-site-layout__content">
<NavigableRegion
ariaLabel={ __( 'Navigation', 'woocommerce' ) }
className="edit-site-layout__sidebar-region"
<HighlightedBlockContextProvider>
<EntityProvider kind="root" type="site">
<EntityProvider
kind="postType"
type={ templateType }
id={ templateId }
>
<motion.div
animate={ { opacity: 1 } }
transition={ {
type: 'tween',
duration:
// Disable transitiont in mobile to emulate a full page transition.
disableMotion || isMobileViewport
? 0
: ANIMATION_DURATION,
ease: 'easeOut',
} }
className="edit-site-layout__sidebar"
>
<Sidebar />
</motion.div>
</NavigableRegion>
<div className={ classnames( 'edit-site-layout' ) }>
<motion.div
className="edit-site-layout__header-container"
animate={ 'view' }
>
<SiteHub
as={ motion.div }
variants={ {
view: { x: 0 },
} }
isTransparent={ false }
className="edit-site-layout__hub"
/>
</motion.div>
{ ! isMobileViewport && (
<div
className={ classnames(
'edit-site-layout__canvas-container'
) }
>
{ canvasResizer }
{ !! canvasSize.width && (
<motion.div
whileHover={ {
scale: 1.005,
transition: {
duration: disableMotion ? 0 : 0.5,
ease: 'easeOut',
},
} }
initial={ false }
layout="position"
className={ classnames(
'edit-site-layout__canvas'
<div className="edit-site-layout__content">
<NavigableRegion
ariaLabel={ __(
'Navigation',
'woocommerce'
) }
transition={ {
type: 'tween',
duration: disableMotion
? 0
: ANIMATION_DURATION,
ease: 'easeOut',
} }
className="edit-site-layout__sidebar-region"
>
<ErrorBoundary>
<ResizableFrame
isReady={ ! isEditorLoading }
duringGuideTour={
shouldTourBeShown &&
! onboardingTourProps.showWelcomeTour
}
isFullWidth={ false }
defaultSize={ {
width:
canvasSize.width -
24 /* $canvas-padding */,
height: canvasSize.height,
} }
isOversized={
isResizableFrameOversized
}
setIsOversized={
setIsResizableFrameOversized
}
innerContentStyle={ {
background:
gradientValue ??
backgroundColor,
} }
>
<Editor
isLoading={ isEditorLoading }
/>
</ResizableFrame>
</ErrorBoundary>
</motion.div>
) }
<motion.div
animate={ { opacity: 1 } }
transition={ {
type: 'tween',
duration:
// Disable transitiont in mobile to emulate a full page transition.
disableMotion ||
isMobileViewport
? 0
: ANIMATION_DURATION,
ease: 'easeOut',
} }
className="edit-site-layout__sidebar"
>
<Sidebar />
</motion.div>
</NavigableRegion>
{ ! isMobileViewport && (
<div
className={ classnames(
'edit-site-layout__canvas-container'
) }
>
{ canvasResizer }
{ !! canvasSize.width && (
<motion.div
whileHover={ {
scale: 1.005,
transition: {
duration: disableMotion
? 0
: 0.5,
ease: 'easeOut',
},
} }
initial={ false }
layout="position"
className={ classnames(
'edit-site-layout__canvas'
) }
transition={ {
type: 'tween',
duration: disableMotion
? 0
: ANIMATION_DURATION,
ease: 'easeOut',
} }
>
<ErrorBoundary>
<ResizableFrame
isReady={
! isEditorLoading
}
duringGuideTour={
shouldTourBeShown &&
! onboardingTourProps.showWelcomeTour
}
isFullWidth={ false }
defaultSize={ {
width:
canvasSize.width -
24 /* $canvas-padding */,
height: canvasSize.height,
} }
isOversized={
isResizableFrameOversized
}
setIsOversized={
setIsResizableFrameOversized
}
innerContentStyle={ {
background:
gradientValue ??
backgroundColor,
} }
>
<Editor
isLoading={
isEditorLoading
}
/>
</ResizableFrame>
</ErrorBoundary>
</motion.div>
) }
</div>
) }
</div>
</div>
) }
</div>
</div>
{ shouldTourBeShown && (
<OnboardingTour { ...onboardingTourProps } />
) }
{ shouldTourBeShown && (
<OnboardingTour { ...onboardingTourProps } />
) }
</EntityProvider>
</EntityProvider>
</HighlightedBlockContextProvider>
</LogoBlockContext.Provider>
);
};

View File

@ -1,19 +1,64 @@
/* eslint-disable @woocommerce/dependency-group */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { createInterpolateElement } from '@wordpress/element';
import {
useCallback,
createInterpolateElement,
useContext,
useEffect,
} from '@wordpress/element';
import { Link } from '@woocommerce/components';
import { Spinner } from '@wordpress/components';
// @ts-ignore No types for this exist yet.
import { __experimentalBlockPatternsList as BlockPatternList } from '@wordpress/block-editor';
/**
* Internal dependencies
*/
import { SidebarNavigationScreen } from './sidebar-navigation-screen';
import { ADMIN_URL } from '~/utils/admin-settings';
import { usePatternsByCategory } from '../hooks/use-patterns';
import { useEditorBlocks } from '../hooks/use-editor-blocks';
import { HighlightedBlockContext } from '../context/highlighted-block-context';
const SUPPORTED_HEADER_PATTERNS = [
'woocommerce-blocks/header-centered-menu-with-search',
'woocommerce-blocks/header-essential',
'woocommerce-blocks/header-large',
'woocommerce-blocks/header-minimal',
];
export const SidebarNavigationScreenHeader = () => {
const { isLoading, patterns } = usePatternsByCategory( 'woo-commerce' );
const [ blocks, , onChange ] = useEditorBlocks();
const { setHighlightedBlockIndex, resetHighlightedBlockIndex } = useContext(
HighlightedBlockContext
);
useEffect( () => {
setHighlightedBlockIndex( 0 );
}, [ setHighlightedBlockIndex ] );
const headerPatterns = patterns.filter( ( pattern ) =>
SUPPORTED_HEADER_PATTERNS.includes( pattern.name )
);
const onClickHeaderPattern = useCallback(
( _pattern, selectedBlocks ) => {
onChange( [ selectedBlocks[ 0 ], ...blocks.slice( 1 ) ], {
selection: {},
} );
},
[ blocks, onChange ]
);
return (
<SidebarNavigationScreen
title={ __( 'Change your header', 'woocommerce' ) }
onNavigateBackClick={ resetHighlightedBlockIndex }
description={ createInterpolateElement(
__(
"Select a new header from the options below. Your header includes your site's navigation and will be added to every page. You can continue customizing this via the <EditorLink>Editor</EditorLink>.",
@ -30,7 +75,26 @@ export const SidebarNavigationScreenHeader = () => {
) }
content={
<>
<div className="edit-site-sidebar-navigation-screen-patterns__group-header"></div>
<div className="edit-site-sidebar-navigation-screen-patterns__group-header">
{ isLoading && (
<span className="components-placeholder__preview">
<Spinner />
</span>
) }
{ ! isLoading && (
<BlockPatternList
shownPatterns={ headerPatterns }
blockPatterns={ headerPatterns }
onClickPattern={ onClickHeaderPattern }
label={ 'Headers' }
orientation="vertical"
category={ 'header' }
isDraggable={ false }
showTitlesAsTooltip={ true }
/>
) }
</div>
</>
}
/>

View File

@ -40,6 +40,7 @@ export const SidebarNavigationScreen = ( {
footer,
description,
backPath: backPathProp,
onNavigateBackClick,
}: {
isRoot?: boolean;
title: string;
@ -49,6 +50,7 @@ export const SidebarNavigationScreen = ( {
footer?: React.ReactNode;
description?: React.ReactNode;
backPath?: string;
onNavigateBackClick?: () => void;
} ) => {
const { sendEvent } = useContext( CustomizeStoreContext );
const location = useLocation();
@ -75,6 +77,7 @@ export const SidebarNavigationScreen = ( {
{ ! isRoot && (
<SidebarButton
onClick={ () => {
onNavigateBackClick?.();
const backPath =
backPathProp ?? location.state?.backPath;
if ( backPath ) {
@ -93,6 +96,7 @@ export const SidebarNavigationScreen = ( {
{ isRoot && (
<SidebarButton
onClick={ () => {
onNavigateBackClick?.();
sendEvent( 'GO_BACK_TO_DESIGN_WITH_AI' );
} }
icon={ icon }

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add header customization to the Assembler Hub

File diff suppressed because it is too large Load Diff