[Customize your store] Fix site preview in transitional screen (#40588)

* Replace mshot image with preview editor frame in transitional screen

* Add changelog

* Fix test

* Use current xstate to check if it is transitional page
This commit is contained in:
Chi-Hsuan Huang 2023-10-05 20:36:08 +08:00 committed by GitHub
parent a28a552bbf
commit 06ea7ae24a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 115 additions and 321 deletions

View File

@ -47,7 +47,8 @@ export type ScaledBlockPreviewProps = {
};
additionalStyles: string;
onClickNavigationItem: ( event: MouseEvent ) => void;
isNavigable: boolean;
isNavigable?: boolean;
isScrollable?: boolean;
};
function ScaledBlockPreview( {
@ -57,6 +58,7 @@ function ScaledBlockPreview( {
additionalStyles,
onClickNavigationItem,
isNavigable = false,
isScrollable = true,
}: ScaledBlockPreviewProps ) {
const { setLogoBlock } = useContext( LogoBlockContext );
const [ fontFamilies ] = useGlobalSetting(
@ -78,6 +80,7 @@ function ScaledBlockPreview( {
<DisabledProvider value={ true }>
<Iframe
aria-hidden
scrolling={ isScrollable ? 'yes' : 'no' }
tabIndex={ -1 }
readonly={ ! isNavigable }
contentRef={ useRefEffect(

View File

@ -14,15 +14,16 @@ import { privateApis as routerPrivateApis } from '@wordpress/router';
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 { useQuery } from '@woocommerce/navigation';
import { useContext, useCallback } from '@wordpress/element';
/**
* Internal dependencies
*/
import BlockPreview from './block-preview';
import { useCallback } from '@wordpress/element';
import { useEditorBlocks } from './hooks/use-editor-blocks';
import { useScrollOpacity } from './hooks/use-scroll-opacity';
import { useQuery } from '@woocommerce/navigation';
import { CustomizeStoreContext } from './';
const { useHistory } = unlock( routerPrivateApis );
@ -66,6 +67,7 @@ export const BlockEditor = ( {} ) => {
const settings = useSiteEditorSettings();
const [ blocks ] = useEditorBlocks();
const urlParams = useQuery();
const { currentState } = useContext( CustomizeStoreContext );
const scrollDirection =
urlParams.path === '/customize-store/assembler-hub/footer'
@ -124,6 +126,7 @@ export const BlockEditor = ( {} ) => {
settings={ settings }
additionalStyles={ '' }
isNavigable={ false }
isScrollable={ currentState !== 'transitionalScreen' }
onClickNavigationItem={ onClickNavigationItem }
// Don't use sub registry so that we can get the logo block from the main registry on the logo sidebar navigation screen component.
useSubRegistry={ false }

View File

@ -59,9 +59,11 @@ type CustomizeStoreComponentProps = Parameters< CustomizeStoreComponent >[ 0 ];
export const CustomizeStoreContext = createContext< {
sendEvent: CustomizeStoreComponentProps[ 'sendEvent' ];
context: Partial< CustomizeStoreComponentProps[ 'context' ] >;
currentState: CustomizeStoreComponentProps[ 'currentState' ];
} >( {
sendEvent: () => {},
context: {},
currentState: 'assemblerHub',
} );
export type events =

View File

@ -11,7 +11,7 @@ import {
useViewportMatch,
} from '@wordpress/compose';
import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import { useState, useContext } from '@wordpress/element';
import {
// @ts-ignore No types for this exist yet.
__unstableMotion as motion,
@ -45,6 +45,8 @@ import { LogoBlockContext } from './logo-block-context';
import ResizableFrame from './resizable-frame';
import { OnboardingTour, useOnboardingTour } from './onboarding-tour';
import { HighlightedBlockContextProvider } from './context/highlighted-block-context';
import { Transitional } from '../transitional';
import { CustomizeStoreContext } from './';
const { useGlobalStyle } = unlock( blockEditorPrivateApis );
@ -74,6 +76,24 @@ export const Layout = () => {
const { record: template } = useEditedEntityRecord();
const { id: templateId, type: templateType } = template;
const { sendEvent, currentState } = useContext( CustomizeStoreContext );
const editor = <Editor isLoading={ isEditorLoading } />;
if ( currentState === 'transitionalScreen' ) {
return (
<EntityProvider kind="root" type="site">
<EntityProvider
kind="postType"
type={ templateType }
id={ templateId }
>
<Transitional sendEvent={ sendEvent } editor={ editor } />
</EntityProvider>
</EntityProvider>
);
}
return (
<LogoBlockContext.Provider
value={ {
@ -188,11 +208,7 @@ export const Layout = () => {
backgroundColor,
} }
>
<Editor
isLoading={
isEditorLoading
}
/>
{ editor }
</ResizableFrame>
</ErrorBoundary>
</motion.div>

View File

@ -25,11 +25,7 @@ import {
} from './intro';
import { DesignWithAi, events as designWithAiEvents } from './design-with-ai';
import { AssemblerHub, events as assemblerHubEvents } from './assembler-hub';
import {
Transitional,
events as transitionalEvents,
services as transitionalServices,
} from './transitional';
import { events as transitionalEvents } from './transitional';
import { findComponentMeta } from '~/utils/xstate/find-component';
import {
CustomizeStoreComponentMeta,
@ -102,7 +98,6 @@ export const customizeStoreStateMachineActions = {
export const customizeStoreStateMachineServices = {
...introServices,
...transitionalServices,
browserPopstateHandler,
markTaskComplete,
};
@ -264,14 +259,6 @@ export const customizeStoreStateMachineDefinition = createMachine( {
invoke: {
src: 'markTaskComplete',
onDone: {
target: 'waitForSitePreview',
},
},
},
waitForSitePreview: {
after: {
// Wait for 5 seconds before redirecting to the transitional page. This is to ensure that the site preview image is refreshed.
5000: {
target: '#customizeStore.transitionalScreen',
},
},
@ -279,8 +266,6 @@ export const customizeStoreStateMachineDefinition = createMachine( {
},
on: {
FINISH_CUSTOMIZATION: {
// Pre-fetch the site preview image for the site for transitional page.
actions: [ 'prefetchSitePreview' ],
target: '.postAssemblerHub',
},
GO_BACK_TO_DESIGN_WITH_AI: {
@ -291,7 +276,7 @@ export const customizeStoreStateMachineDefinition = createMachine( {
transitionalScreen: {
entry: [ { type: 'updateQueryStep', step: 'transitional' } ],
meta: {
component: Transitional,
component: AssemblerHub,
},
on: {
GO_BACK_TO_HOME: {
@ -368,6 +353,7 @@ export const CustomizeStoreController = ( {
parentMachine={ service }
sendEvent={ send }
context={ state.context }
currentState={ state.value }
/>
) : (
<div />

View File

@ -1,6 +1,9 @@
/* eslint-disable @woocommerce/dependency-group */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/**
* External dependencies
*/
import classNames from 'classnames';
import { __ } from '@wordpress/i18n';
import { getSetting } from '@woocommerce/settings';
import { recordEvent } from '@woocommerce/tracks';
@ -10,31 +13,28 @@ import {
// @ts-ignore No types for this exist yet.
__unstableMotion as motion,
} from '@wordpress/components';
// @ts-ignore No types for this exist yet.
import { useIsSiteEditorLoading } from '@wordpress/edit-site/build-module/components/layout/hooks';
/**
* Internal dependencies
*/
import { SiteHub } from '../assembler-hub/site-hub';
import { MShotsImage } from './mshots-image';
import { ADMIN_URL } from '~/utils/admin-settings';
import './style.scss';
export * as services from './services';
export type events = { type: 'GO_BACK_TO_HOME' };
export const PREVIEW_IMAGE_OPTION = {
vpw: 1200,
vph: 742,
w: 588,
h: 363.58,
requeue: true,
};
export const Transitional = ( {
editor,
sendEvent,
}: {
editor: React.ReactNode;
sendEvent: ( event: events ) => void;
} ) => {
const homeUrl: string = getSetting( 'homeUrl', '' );
const isEditorLoading = useIsSiteEditorLoading();
return (
<div className="woocommerce-customize-store__transitional">
@ -70,16 +70,15 @@ export const Transitional = ( {
{ __( 'Preview store', 'woocommerce' ) }
</Button>
<div className="woocommerce-customize-store__transitional-site-img-container">
<MShotsImage
url={ homeUrl }
alt={ __( 'Your store screenshot', 'woocommerce' ) }
aria-labelledby={ __(
'Your store screenshot',
'woocommerce'
) }
options={ PREVIEW_IMAGE_OPTION }
/>
<div
className={ classNames(
'woocommerce-customize-store__transitional-site-preview-container',
{
'is-loading': isEditorLoading,
}
) }
>
{ editor }
</div>
<div className="woocommerce-customize-store__transitional-actions">
<div className="woocommerce-customize-store__transitional-action">

View File

@ -1,215 +0,0 @@
// See https://github.com/Automattic/wp-calypso/blob/6923bed938911a931e722a1efa6fcbbf942677a9/packages/onboarding/src/mshots-image/index.tsx
// TODO: @automattic/onboarding is not published to npm, so we can't use it here. We'll need to add the "requeue" option to MShotsOptions and we could remove this when it's published.
/**
* External dependencies
*/
import { addQueryArgs } from '@wordpress/url';
import classnames from 'classnames';
import { useState, useEffect, useRef } from 'react';
interface MShotsImageProps {
url: string;
alt: string;
'aria-labelledby': string;
options: MShotsOptions;
scrollable?: boolean;
}
export type MShotsOptions = {
vpw: number;
vph: number;
w: number;
h?: number;
screen_height?: number;
format?: 'png' | 'jpeg';
requeue?: boolean;
};
// eslint-disable-next-line no-console
const debug = console.debug;
export function getMshotsUrl(
targetUrl: string,
options: MShotsOptions,
count = 0
): string {
const mshotsUrl = 'https://s0.wp.com/mshots/v1/';
const mshotsRequest = addQueryArgs(
mshotsUrl + encodeURIComponent( targetUrl ),
{
...options,
count,
}
);
return mshotsRequest;
}
const MAXTRIES = 10;
// This custom react hook returns undefined while the image is loading and
// a HTMLImageElement (i.e. the class you get from `new Image()`) once loading
// is complete.
//
// It also triggers a re-render (via setState()) when the value changes, so just
// check if it's truthy and then treat it like any other Image.
//
// Note the loading may occur immediately and synchronously if the image is
// already or may take up to several seconds if mshots has to generate and cache
// new images.
//
// The calling code doesn't need to worry about the details except that you'll
// want some sort of loading display.
//
// Inspired by https://stackoverflow.com/a/60458593
const useMshotsImg = (
src: string,
options: MShotsOptions
): HTMLImageElement | undefined => {
const [ loadedImg, setLoadedImg ] = useState< HTMLImageElement >();
const [ count, setCount ] = useState( 0 );
const previousSrc = useRef( src );
const imgRef = useRef< HTMLImageElement >();
const timeoutIdRef = useRef< number >();
const previousImg = useRef< HTMLImageElement >();
const previousOptions = useRef< MShotsOptions >();
// Oddly, we need to assign to current here after ref creation in order to
// pass the equivalence check and avoid a spurious reset
previousOptions.current = options;
// Note: Mshots doesn't care about the "count" param, but it is important
// to browser caching. Getting this wrong looks like the url resolving
// before the image is ready.
useEffect( () => {
// If there's been a "props" change we need to reset everything:
if (
options !== previousOptions.current ||
( src !== previousSrc.current && imgRef.current )
) {
// Make sure an old image can't trigger a spurious state update
debug( 'resetting mShotsUrl request' );
if ( src !== previousSrc.current ) {
debug( 'src changed\nfrom', previousSrc.current, '\nto', src );
}
if ( options !== previousOptions.current ) {
debug(
'options changed\nfrom',
previousOptions.current,
'\nto',
options
);
}
if ( previousImg.current && previousImg.current.onload ) {
previousImg.current.onload = null;
if ( timeoutIdRef.current ) {
clearTimeout( timeoutIdRef.current );
timeoutIdRef.current = undefined;
}
}
setLoadedImg( undefined );
setCount( 0 );
previousImg.current = imgRef.current;
previousOptions.current = options;
previousSrc.current = src;
}
const srcUrl = getMshotsUrl( src, options, count );
const newImage = new Image();
newImage.onload = () => {
// Detect default image (Don't request a 400x300 image).
//
// If this turns out to be a problem, it might help to know that the
// http request status for the default is a 307. Unfortunately we
// don't get the request through an img element so we'd need to
// take a completely different approach using ajax.
if (
newImage.naturalWidth !== 400 ||
newImage.naturalHeight !== 300
) {
// Note we're using the naked object here, not the ref, because
// this is the callback on the image itself. We'd never want
// the image to finish loading and set some other image.
setLoadedImg( newImage );
} else if ( count < MAXTRIES ) {
// Only refresh 10 times
// Triggers a target.src change with increasing timeouts
timeoutIdRef.current = window.setTimeout(
() => setCount( ( _count ) => _count + 1 ),
count * 500
);
}
};
newImage.src = srcUrl;
imgRef.current = newImage;
return () => {
if ( imgRef.current && imgRef.current.onload ) {
imgRef.current.onload = null;
}
clearTimeout( timeoutIdRef.current );
};
}, [ src, count, options ] );
return loadedImg;
};
// For hover-scroll, we use a div with a background image (rather than an img element)
// in order to use transitions between `top` and `bottom` on the
// `background-position` property.
// The "normal" top & bottom properties are problematic individually because we
// don't know how big the images will be, and using both gets the
// right positions but with no transition (as they're different properties).
export const MShotsImage = ( {
url,
'aria-labelledby': labelledby,
alt,
options,
scrollable = false,
}: MShotsImageProps ) => {
const maybeImage = useMshotsImg( url, options );
const src: string = maybeImage?.src || '';
const visible = !! src;
const backgroundImage = maybeImage?.src && `url( ${ maybeImage?.src } )`;
const animationScrollSpeedInPixelsPerSecond = 400;
const animationDuration =
( maybeImage?.naturalHeight || 600 ) /
animationScrollSpeedInPixelsPerSecond;
const scrollableStyles = {
backgroundImage,
transition: `background-position ${ animationDuration }s`,
};
const style = {
...( scrollable ? scrollableStyles : {} ),
};
const className = classnames(
'mshots-image__container',
scrollable && 'hover-scroll',
visible ? 'mshots-image-visible' : 'mshots-image__loader'
);
// The "! visible" here is only to dodge a particularly specific css
// rule effecting the placeholder while loading static images:
// '.design-picker .design-picker__image-frame img { ..., height: auto }'
return scrollable || ! visible ? (
<div
className={ className }
style={ style }
aria-labelledby={ labelledby }
/>
) : (
<img
{ ...{ className, style, src, alt } }
aria-labelledby={ labelledby }
alt={ alt }
/>
);
};
export default MShotsImage;

View File

@ -1,19 +0,0 @@
/**
* External dependencies
*/
import { getSetting } from '@woocommerce/settings';
/**
* Internal dependencies
*/
import { getMshotsUrl } from './mshots-image';
import { PREVIEW_IMAGE_OPTION } from './';
export const fetchSitePreviewImage = async () => {
const homeUrl: string = getSetting( 'homeUrl', '' );
return window
.fetch( getMshotsUrl( homeUrl, PREVIEW_IMAGE_OPTION ) )
.catch( () => {
// Ignore errors
} );
};

View File

@ -67,25 +67,59 @@
margin: 20px 0 0;
}
.woocommerce-customize-store__transitional-site-img-container {
width: 600px;
height: 371px;
.woocommerce-customize-store__transitional-site-preview-container {
border-radius: 16px;
margin-top: 50px;
background: #f6f7f7;
box-shadow: 0 6px 6px 0 rgba(0, 0, 0, 0.02), 0 13px 10px 0 rgba(0, 0, 0, 0.03), 0 15px 20px 0 rgba(0, 0, 0, 0.04);
margin-top: 50px;
display: flex;
align-items: center;
justify-content: center;
width: 600px;
height: 371px;
.mshots-image__loader {
width: 600px;
height: 371px;
border-radius: 16px;
div {
position: relative;
border-radius: 24px;
}
img {
border-radius: 16px;
.woocommerce-customize-store__edit-site-editor {
height: 100%;
}
.woocommerce-customize-store__block-editor {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.interface-navigable-region {
overflow: hidden;
}
.auto-block-preview__container {
width: 588px;
height: 363px;
position: relative;
}
iframe {
border-radius: 24px;
width: 1176px;
height: 726px;
transform: scale(0.5);
left: -50%;
position: relative;
top: -50%;
}
&.is-loading {
@include placeholder();
iframe {
visibility: hidden;
opacity: 0.5;
}
}
}
@ -133,22 +167,3 @@
}
}
}
// mshots component
.mshots-image__loader {
@include placeholder();
}
.mshots-image-visible {
animation: fadein 300ms;
}
@keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

View File

@ -10,13 +10,6 @@ import { recordEvent } from '@woocommerce/tracks';
*/
import { Transitional } from '../index';
jest.mock( '../mshots-image', () => ( {
__esModule: true,
MShotsImage: () => {
return <img alt="preview-img" />;
},
} ) );
jest.mock( '../../assembler-hub/site-hub', () => ( {
__esModule: true,
SiteHub: () => {
@ -24,6 +17,14 @@ jest.mock( '../../assembler-hub/site-hub', () => ( {
},
} ) );
jest.mock(
'@wordpress/edit-site/build-module/components/layout/hooks',
() => ( {
__esModule: true,
useIsSiteEditorLoading: jest.fn().mockReturnValue( false ),
} )
);
jest.mock( '@woocommerce/tracks', () => ( { recordEvent: jest.fn() } ) );
describe( 'Transitional', () => {
@ -45,8 +46,6 @@ describe( 'Transitional', () => {
screen.getByText( /Your store looks great!/i )
).toBeInTheDocument();
expect( screen.getByRole( 'img' ) ).toBeInTheDocument();
expect(
screen.getByRole( 'button', {
name: /Preview store/i,

View File

@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { AnyInterpreter, Sender } from 'xstate';
import { AnyInterpreter, Sender, StateValue } from 'xstate';
/**
* Internal dependencies
@ -13,6 +13,7 @@ export type CustomizeStoreComponent = ( props: {
parentMachine: AnyInterpreter;
sendEvent: Sender< customizeStoreStateMachineEvents >;
context: customizeStoreStateMachineContext;
currentState: StateValue;
} ) => React.ReactElement | null;
export type CustomizeStoreComponentMeta = {

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Fix customize your store site preview in transitional screen