Revert "CYS: Revert Zoom Feature (#50535)"

This reverts commit 16ef869587.
This commit is contained in:
Tom Cafferkey 2024-09-02 11:09:18 +01:00
parent 0f7773dd47
commit 4ff24a0188
14 changed files with 611 additions and 57 deletions

View File

@ -0,0 +1,145 @@
/* eslint-disable @woocommerce/dependency-group */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/**
* External dependencies
*/
import clsx from 'clsx';
import React from 'react';
import { useDispatch, useSelect } from '@wordpress/data';
import { useContext } from '@wordpress/element';
// @ts-ignore No types for this exist yet.
import { store as editorStore } from '@wordpress/editor';
import { Icon, desktop, tablet, mobile } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import {
// @ts-ignore No types for this exist yet.
NavigableMenu,
Circle,
SVG,
Path,
} from '@wordpress/components';
/**
* Internal dependencies
*/
import { ZoomOutContext } from '../context/zoom-out-context';
const zoomIn = (
<SVG width="24" height="24" viewBox="0 0 24 24">
<Circle cx="11" cy="11" r="7.25" fill="transparent" strokeWidth="1.5" />
<Path d="M8 11H14M11 8V14" strokeWidth="1.5" />
<Path d="M16 16L20 20" strokeWidth="1.5" />
</SVG>
);
const zoomOut = (
<SVG width="24" height="24" viewBox="0 0 24 24">
<Circle cx="11" cy="11" r="7.25" fill="transparent" strokeWidth="1.5" />
<Path d="M16 16L20 20" strokeWidth="1.5" />
<Path d="M8 11H14" strokeWidth="1.5" />
</SVG>
);
const BUTTON_CLASS_NAMES =
'components-button has-icon woocommerce-customize-store__device-button';
const ICON_CLASS_NAMES = 'woocommerce-customize-store__device-icon';
export function DeviceToolbar( { isEditorLoading = false } ) {
// @ts-expect-error expect error
const { setDeviceType } = useDispatch( editorStore );
const { toggleZoomOut, isZoomedOut } = useContext( ZoomOutContext );
const { deviceType } = useSelect( ( select ) => {
// @ts-ignore expect error
const { getDeviceType } = select( editorStore );
return {
deviceType: getDeviceType(),
};
} );
// Zoom Out isn't available on mobile or tablet.
// In this case, we want to switch to desktop mode when zooming out.
const switchDeviceType = ( newDeviceType: string ) => {
if ( isZoomedOut ) {
toggleZoomOut();
}
setDeviceType( newDeviceType );
};
return (
<NavigableMenu
className="woocommerce-customize-store__device-toolbar"
orientation="horizontal"
role="toolbar"
aria-label={ __( 'Resize Options', 'woocommerce' ) }
>
<button
disabled={ isEditorLoading }
className={ clsx( BUTTON_CLASS_NAMES, {
'is-selected': deviceType === 'Desktop',
} ) }
aria-label="Desktop View"
onClick={ () => {
switchDeviceType( 'Desktop' );
} }
>
<Icon
icon={ desktop }
size={ 30 }
className={ clsx( ICON_CLASS_NAMES ) }
/>
</button>
<button
disabled={ isEditorLoading }
className={ clsx( BUTTON_CLASS_NAMES, {
'is-selected': deviceType === 'Tablet',
} ) }
aria-label="Tablet View"
onClick={ () => {
switchDeviceType( 'Tablet' );
} }
>
<Icon
icon={ tablet }
size={ 30 }
className={ clsx( ICON_CLASS_NAMES ) }
/>
</button>
<button
disabled={ isEditorLoading }
className={ clsx( BUTTON_CLASS_NAMES, {
'is-selected': deviceType === 'Mobile',
} ) }
aria-label="Mobile View"
onClick={ () => {
switchDeviceType( 'Mobile' );
} }
>
<Icon
icon={ mobile }
size={ 30 }
className={ clsx( ICON_CLASS_NAMES ) }
/>
</button>
<button
disabled={ isEditorLoading }
className={ clsx( BUTTON_CLASS_NAMES ) }
aria-label={ isZoomedOut ? 'Zoom In View' : 'Zoom Out View' }
onClick={ () => {
setDeviceType( 'Desktop' );
toggleZoomOut();
} }
>
<Icon
icon={ isZoomedOut ? zoomIn : zoomOut }
size={ 30 }
className={ clsx(
ICON_CLASS_NAMES,
`${ ICON_CLASS_NAMES }--zoom`
) }
/>
</button>
</NavigableMenu>
);
}

View File

@ -0,0 +1,39 @@
/**
* External dependencies
*/
import React, { createContext, useState } from '@wordpress/element';
import type { ReactNode } from 'react';
export const ZoomOutContext = createContext< {
isZoomedOut: boolean;
toggleZoomOut: () => void;
} >( {
isZoomedOut: false,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
toggleZoomOut: () => {
// No op by default.
},
} );
export const ZoomOutContextProvider = ( {
children,
}: {
children: ReactNode;
} ) => {
const [ isZoomedOut, setIsZoomedOut ] = useState< boolean >( false );
const toggleZoomOut = () => {
setIsZoomedOut( ! isZoomedOut );
};
return (
<ZoomOutContext.Provider
value={ {
isZoomedOut,
toggleZoomOut,
} }
>
{ children }
</ZoomOutContext.Provider>
);
};

View File

@ -58,7 +58,7 @@ export const usePopoverHandler = () => {
hoveredBlockClientId: string | null; hoveredBlockClientId: string | null;
} ) => { } ) => {
const iframe = window.document.querySelector( const iframe = window.document.querySelector(
'.woocommerce-customize-store-assembler > iframe[name="editor-canvas"]' '.woocommerce-customize-store-assembler > .block-editor-iframe__container iframe[name="editor-canvas"]'
) as HTMLElement; ) as HTMLElement;
const target = event.target as HTMLElement; const target = event.target as HTMLElement;

View File

@ -1,5 +1,7 @@
// Reference: https://github.com/WordPress/gutenberg/blob/f91b4fb4a12e41dd39c9594f24ea1a1a4e23dade/packages/block-editor/src/components/iframe/index.js#L1 // Reference: https://github.com/WordPress/gutenberg/blob/f91b4fb4a12e41dd39c9594f24ea1a1a4e23dade/packages/block-editor/src/components/iframe/index.js#L1
// We fork the code from the above link to reduce the unnecessary network requests and improve the performance. // We fork the code from the above link to reduce the unnecessary network requests and improve the performance.
// Some of the code is not used in the project and is removed.
// We've also made some changes to the code to make it work with the Zoom Out feature.
/** /**
* External dependencies * External dependencies
@ -11,6 +13,8 @@ import {
forwardRef, forwardRef,
useMemo, useMemo,
useEffect, useEffect,
useRef,
useContext,
} from '@wordpress/element'; } from '@wordpress/element';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { import {
@ -21,37 +25,42 @@ import {
} from '@wordpress/compose'; } from '@wordpress/compose';
import { __experimentalStyleProvider as StyleProvider } from '@wordpress/components'; import { __experimentalStyleProvider as StyleProvider } from '@wordpress/components';
import { useSelect } from '@wordpress/data'; import { useSelect } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as blockEditorStore } from '@wordpress/block-editor';
/**
* Internal dependencies
*/
import { ZoomOutContext } from './context/zoom-out-context';
function Iframe( { function Iframe( {
contentRef, contentRef,
children, children,
tabIndex = 0, tabIndex = 0,
scale = 1, scale = 1,
frameSize = 0, frameSize = 0,
expand = false,
readonly, readonly,
forwardedRef: ref, forwardedRef: ref,
loadStyles = true, title = __( 'Editor canvas', 'woocommerce' ),
loadScripts = false, canEnableZoomOutView = false,
...props ...props
} ) { } ) {
const [ iframeDocument, setIframeDocument ] = useState();
const [ bodyClasses, setBodyClasses ] = useState( [] );
const { resolvedAssets } = useSelect( ( select ) => { const { resolvedAssets } = useSelect( ( select ) => {
const settings = select( blockEditorStore ).getSettings(); const { getSettings } = select( blockEditorStore );
const settings = getSettings();
return { return {
resolvedAssets: settings.__unstableResolvedAssets, resolvedAssets: settings.__unstableResolvedAssets,
}; };
}, [] ); }, [] );
const { styles = '', scripts = '' } = resolvedAssets;
const { styles = '', scripts = '' } = resolvedAssets;
const [ iframeDocument, setIframeDocument ] = useState();
const prevContainerWidth = useRef( 0 );
const [ bodyClasses, setBodyClasses ] = useState( [] );
const [ contentResizeListener, { height: contentHeight } ] = const [ contentResizeListener, { height: contentHeight } ] =
useResizeObserver(); useResizeObserver();
const [ containerResizeListener, { width: containerWidth } ] =
useResizeObserver();
const setRef = useRefEffect( ( node ) => { const setRef = useRefEffect( ( node ) => {
node._load = () => { node._load = () => {
setIframeDocument( node.contentDocument ); setIframeDocument( node.contentDocument );
@ -62,6 +71,9 @@ function Iframe( {
documentElement.classList.add( 'block-editor-iframe__html' ); documentElement.classList.add( 'block-editor-iframe__html' );
// Ideally ALL classes that are added through get_body_class should
// be added in the editor too, which we'll somehow have to get from
// the server in the future (which will run the PHP filters).
setBodyClasses( setBodyClasses(
Array.from( ownerDocument?.body.classList ).filter( Array.from( ownerDocument?.body.classList ).filter(
( name ) => ( name ) =>
@ -70,6 +82,8 @@ function Iframe( {
name === 'wp-embed-responsive' name === 'wp-embed-responsive'
) )
); );
contentDocument.dir = ownerDocument.dir;
} }
node.addEventListener( 'load', onLoad ); node.addEventListener( 'load', onLoad );
@ -80,8 +94,53 @@ function Iframe( {
}; };
}, [] ); }, [] );
const [ iframeWindowInnerHeight, setIframeWindowInnerHeight ] = useState();
const iframeResizeRef = useRefEffect( ( node ) => {
const nodeWindow = node.ownerDocument.defaultView;
setIframeWindowInnerHeight( nodeWindow.innerHeight );
const onResize = () => {
setIframeWindowInnerHeight( nodeWindow.innerHeight );
};
nodeWindow.addEventListener( 'resize', onResize );
return () => {
nodeWindow.removeEventListener( 'resize', onResize );
};
}, [] );
const [ windowInnerWidth, setWindowInnerWidth ] = useState();
const windowResizeRef = useRefEffect( ( node ) => {
const nodeWindow = node.ownerDocument.defaultView;
setWindowInnerWidth( nodeWindow.innerWidth );
const onResize = () => {
setWindowInnerWidth( nodeWindow.innerWidth );
};
nodeWindow.addEventListener( 'resize', onResize );
return () => {
nodeWindow.removeEventListener( 'resize', onResize );
};
}, [] );
const isZoomedOut = scale !== 1 && canEnableZoomOutView;
useEffect( () => {
if ( ! isZoomedOut && ! prevContainerWidth.current ) {
prevContainerWidth.current = containerWidth;
}
}, [ containerWidth, isZoomedOut ] );
const disabledRef = useDisabled( { isDisabled: ! readonly } ); const disabledRef = useDisabled( { isDisabled: ! readonly } );
const bodyRef = useMergeRefs( [ contentRef, disabledRef ] ); const bodyRef = useMergeRefs( [
contentRef,
disabledRef,
// Avoid resize listeners when not needed, these will trigger
// unnecessary re-renders when animating the iframe width, or when
// expanding preview iframes.
isZoomedOut ? iframeResizeRef : null,
] );
// Correct doctype is required to enable rendering in standards // Correct doctype is required to enable rendering in standards
// mode. Also preload the styles to avoid a flash of unstyled // mode. Also preload the styles to avoid a flash of unstyled
@ -89,10 +148,23 @@ function Iframe( {
const html = `<!doctype html> const html = `<!doctype html>
<html> <html>
<head> <head>
<meta charset="utf-8">
<script>window.frameElement._load()</script> <script>window.frameElement._load()</script>
<style>html{height:auto!important;min-height:100%;}body{margin:0}</style> <style>
${ loadStyles ? styles : '' } html{
${ loadScripts ? scripts : '' } height: auto !important;
min-height: 100%;
}
/* Lowest specificity to not override global styles */
:where(body) {
margin: 0;
/* Default background color in case zoom out mode background
colors the html element */
background-color: white;
}
</style>
${ styles }
${ scripts }
</head> </head>
<body> <body>
<script>document.currentScript.parentElement.remove()</script> <script>document.currentScript.parentElement.remove()</script>
@ -108,30 +180,86 @@ function Iframe( {
useEffect( () => cleanup, [ cleanup ] ); useEffect( () => cleanup, [ cleanup ] );
// We need to counter the margin created by scaling the iframe. If the scale useEffect( () => {
// is e.g. 0.45, then the top + bottom margin is 0.55 (1 - scale). Just the if ( ! canEnableZoomOutView || ! iframeDocument || ! isZoomedOut ) {
// top or bottom margin is 0.55 / 2 ((1 - scale) / 2). return;
const marginFromScaling = ( contentHeight * ( 1 - scale ) ) / 2; }
return ( const maxWidth = 800;
iframeDocument.documentElement.classList.add( 'is-zoomed-out' );
iframeDocument.documentElement.style.setProperty(
'--wp-block-editor-iframe-zoom-out-scale',
scale === 'default'
? Math.min( containerWidth, maxWidth ) /
prevContainerWidth.current
: scale
);
iframeDocument.documentElement.style.setProperty(
'--wp-block-editor-iframe-zoom-out-frame-size',
typeof frameSize === 'number' ? `${ frameSize }px` : frameSize
);
iframeDocument.documentElement.style.setProperty(
'--wp-block-editor-iframe-zoom-out-content-height',
`${ contentHeight }px`
);
iframeDocument.documentElement.style.setProperty(
'--wp-block-editor-iframe-zoom-out-inner-height',
`${ iframeWindowInnerHeight }px`
);
iframeDocument.documentElement.style.setProperty(
'--wp-block-editor-iframe-zoom-out-container-width',
`${ containerWidth }px`
);
iframeDocument.documentElement.style.setProperty(
'--wp-block-editor-iframe-zoom-out-prev-container-width',
`${ prevContainerWidth.current }px`
);
return () => {
iframeDocument.documentElement.classList.remove( 'is-zoomed-out' );
iframeDocument.documentElement.style.removeProperty(
'--wp-block-editor-iframe-zoom-out-scale'
);
iframeDocument.documentElement.style.removeProperty(
'--wp-block-editor-iframe-zoom-out-frame-size'
);
iframeDocument.documentElement.style.removeProperty(
'--wp-block-editor-iframe-zoom-out-content-height'
);
iframeDocument.documentElement.style.removeProperty(
'--wp-block-editor-iframe-zoom-out-inner-height'
);
iframeDocument.documentElement.style.removeProperty(
'--wp-block-editor-iframe-zoom-out-container-width'
);
iframeDocument.documentElement.style.removeProperty(
'--wp-block-editor-iframe-zoom-out-prev-container-width'
);
};
}, [
scale,
frameSize,
iframeDocument,
iframeWindowInnerHeight,
contentHeight,
containerWidth,
windowInnerWidth,
isZoomedOut,
canEnableZoomOutView,
] );
const iframe = (
<> <>
{ /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */ }
<iframe <iframe
{ ...props } { ...props }
style={ { style={ {
border: 0,
...props.style, ...props.style,
height: expand ? contentHeight : props.style?.height, height: props.style?.height,
marginTop:
scale !== 1
? -marginFromScaling + frameSize
: props.style?.marginTop,
marginBottom:
scale !== 1
? -marginFromScaling + frameSize
: props.style?.marginBottom,
transform:
scale !== 1
? `scale( ${ scale } )`
: props.style?.transform,
transition: 'all .3s', transition: 'all .3s',
} } } }
ref={ useMergeRefs( [ ref, setRef ] ) } ref={ useMergeRefs( [ ref, setRef ] ) }
@ -140,7 +268,7 @@ function Iframe( {
// mode. Also preload the styles to avoid a flash of unstyled // mode. Also preload the styles to avoid a flash of unstyled
// content. // content.
src={ src } src={ src }
title={ __( 'Editor canvas', 'woocommerce' ) } title={ title }
name="editor-canvas" name="editor-canvas"
> >
{ iframeDocument && { iframeDocument &&
@ -163,6 +291,26 @@ function Iframe( {
</iframe> </iframe>
</> </>
); );
return (
<div className="block-editor-iframe__container" ref={ windowResizeRef }>
{ containerResizeListener }
<div
className={ clsx(
'block-editor-iframe__scale-container',
isZoomedOut && 'is-zoomed-out'
) }
style={ {
'--wp-block-editor-iframe-zoom-out-container-width':
isZoomedOut && `${ containerWidth }px`,
'--wp-block-editor-iframe-zoom-out-prev-container-width':
isZoomedOut && `${ prevContainerWidth.current }px`,
} }
>
{ iframe }
</div>
</div>
);
} }
function IframeIfReady( props, ref ) { function IframeIfReady( props, ref ) {
@ -172,6 +320,15 @@ function IframeIfReady( props, ref ) {
[] []
); );
const { isZoomedOut } = useContext( ZoomOutContext );
const zoomOutProps = isZoomedOut
? {
scale: 'default',
frameSize: '48px',
}
: {};
// We shouldn't render the iframe until the editor settings are initialised. // We shouldn't render the iframe until the editor settings are initialised.
// The initial settings are needed to get the styles for the srcDoc, which // The initial settings are needed to get the styles for the srcDoc, which
// cannot be changed after the iframe is mounted. srcDoc is used to to set // cannot be changed after the iframe is mounted. srcDoc is used to to set
@ -181,7 +338,12 @@ function IframeIfReady( props, ref ) {
return null; return null;
} }
return <Iframe { ...props } forwardedRef={ ref } />; const iframeProps = {
...props,
...zoomOutProps,
};
return <Iframe { ...iframeProps } forwardedRef={ ref } />;
} }
export default forwardRef( IframeIfReady ); export default forwardRef( IframeIfReady );

View File

@ -51,6 +51,8 @@ import { getNewPath } from '@woocommerce/navigation';
import useBodyClass from '../hooks/use-body-class'; import useBodyClass from '../hooks/use-body-class';
import { OptInSubscribe } from './opt-in/opt-in'; import { OptInSubscribe } from './opt-in/opt-in';
import { OptInContextProvider } from './opt-in/context'; import { OptInContextProvider } from './opt-in/context';
import { ZoomOutContextProvider } from './context/zoom-out-context';
import './tracking'; import './tracking';
const { RouterProvider } = unlock( routerPrivateApis ); const { RouterProvider } = unlock( routerPrivateApis );
@ -150,7 +152,15 @@ const initializeAssembleHub = () => {
export const AssemblerHub: CustomizeStoreComponent = ( props ) => { export const AssemblerHub: CustomizeStoreComponent = ( props ) => {
const isInitializedRef = useRef( false ); const isInitializedRef = useRef( false );
// @ts-expect-error temp fix
const isAiFlow = window.parent?.window.cys_aiFlow ? true : false;
useBodyClass( 'woocommerce-assembler' ); useBodyClass( 'woocommerce-assembler' );
useBodyClass(
isAiFlow
? 'woocommerce-assembler--with-ai'
: 'woocommerce-assembler--without-ai'
);
if ( ! isInitializedRef.current ) { if ( ! isInitializedRef.current ) {
initializeAssembleHub(); initializeAssembleHub();
@ -184,7 +194,9 @@ export const AssemblerHub: CustomizeStoreComponent = ( props ) => {
<OptInContextProvider> <OptInContextProvider>
<GlobalStylesProvider> <GlobalStylesProvider>
<RouterProvider> <RouterProvider>
<Layout /> <ZoomOutContextProvider>
<Layout />
</ZoomOutContextProvider>
</RouterProvider> </RouterProvider>
<OptInSubscribe /> <OptInSubscribe />
</GlobalStylesProvider> </GlobalStylesProvider>

View File

@ -34,6 +34,9 @@ import { NavigableRegion } from '@wordpress/interface';
import { EntityProvider } from '@wordpress/core-data'; import { EntityProvider } from '@wordpress/core-data';
// @ts-ignore No types for this exist yet. // @ts-ignore No types for this exist yet.
import useEditedEntityRecord from '@wordpress/edit-site/build-module/components/use-edited-entity-record'; import useEditedEntityRecord from '@wordpress/edit-site/build-module/components/use-edited-entity-record';
// @ts-ignore No types for this exist yet.
import { store as editorStore } from '@wordpress/editor';
import { useSelect } from '@wordpress/data';
/** /**
* Internal dependencies * Internal dependencies
@ -52,12 +55,14 @@ import { useQuery } from '@woocommerce/navigation';
import { FlowType } from '../types'; import { FlowType } from '../types';
import { isOfflineAIFlow } from '../guards'; import { isOfflineAIFlow } from '../guards';
import { isWooExpress } from '~/utils/is-woo-express'; import { isWooExpress } from '~/utils/is-woo-express';
import { isFullComposabilityFeatureAndAPIAvailable } from './utils/is-full-composability-enabled';
import { trackEvent } from '../tracking'; import { trackEvent } from '../tracking';
import { SidebarNavigationExtraScreen } from './sidebar/navigation-extra-screen/sidebar-navigation-extra-screen'; import { SidebarNavigationExtraScreen } from './sidebar/navigation-extra-screen/sidebar-navigation-extra-screen';
import { DeviceToolbar } from './components/device-toolbar';
const { useGlobalStyle } = unlock( blockEditorPrivateApis ); const { useGlobalStyle } = unlock( blockEditorPrivateApis );
const ANIMATION_DURATION = 0.5; const ANIMATION_DURATION = 0.3;
export const Layout = () => { export const Layout = () => {
const [ logoBlockIds, setLogoBlockIds ] = useState< Array< string > >( [] ); const [ logoBlockIds, setLogoBlockIds ] = useState< Array< string > >( [] );
@ -72,6 +77,15 @@ export const Layout = () => {
isOfflineAIFlow( context.flowType ) && customizing !== 'true' isOfflineAIFlow( context.flowType ) && customizing !== 'true'
); );
const { deviceType } = useSelect( ( select ) => {
// @ts-ignore No types for this exist yet.
const { getDeviceType } = select( editorStore );
return {
deviceType: getDeviceType(),
};
} );
useEffect( () => { useEffect( () => {
setShowAiOfflineModal( setShowAiOfflineModal(
isOfflineAIFlow( context.flowType ) && customizing !== 'true' isOfflineAIFlow( context.flowType ) && customizing !== 'true'
@ -203,6 +217,18 @@ export const Layout = () => {
{ ! isMobileViewport && ( { ! isMobileViewport && (
<div className="edit-site-layout__canvas-container"> <div className="edit-site-layout__canvas-container">
{ isFullComposabilityFeatureAndAPIAvailable() && (
<motion.div
initial={ false }
layout="position"
>
<DeviceToolbar
isEditorLoading={
isEditorLoading
}
/>
</motion.div>
) }
{ canvasResizer } { canvasResizer }
{ !! canvasSize.width && ( { !! canvasSize.width && (
<motion.div <motion.div
@ -234,11 +260,17 @@ export const Layout = () => {
setIsOversized={ setIsOversized={
setIsResizableFrameOversized setIsResizableFrameOversized
} }
isResizingHandleEnabled={
! isFullComposabilityFeatureAndAPIAvailable()
}
innerContentStyle={ { innerContentStyle={ {
background: background:
gradientValue ?? gradientValue ??
backgroundColor, backgroundColor,
} } } }
deviceType={
deviceType
}
> >
{ editor } { editor }
</ResizableFrame> </ResizableFrame>

View File

@ -5,7 +5,7 @@
* External dependencies * External dependencies
*/ */
import clsx from 'clsx'; import clsx from 'clsx';
import { useState, useRef, createContext } from '@wordpress/element'; import { useState, useRef, createContext, useEffect } from '@wordpress/element';
import { import {
ResizableBox, ResizableBox,
Tooltip, Tooltip,
@ -14,6 +14,7 @@ import {
} from '@wordpress/components'; } from '@wordpress/components';
import { useInstanceId } from '@wordpress/compose'; import { useInstanceId } from '@wordpress/compose';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { __experimentalUseResizeCanvas as useResizeCanvas } from '@wordpress/block-editor';
// Removes the inline styles in the drag handles. // Removes the inline styles in the drag handles.
const HANDLE_STYLES_OVERRIDE = { const HANDLE_STYLES_OVERRIDE = {
@ -78,6 +79,9 @@ function ResizableFrame( {
defaultSize, defaultSize,
innerContentStyle, innerContentStyle,
isHandleVisibleByDefault = false, isHandleVisibleByDefault = false,
isResizingHandleEnabled = true,
/** Passing as a prop because the LYS feature does not have access to the editor data store, but CYS feature does. */
deviceType = null,
} ) { } ) {
const [ frameSize, setFrameSize ] = useState( INITIAL_FRAME_SIZE ); const [ frameSize, setFrameSize ] = useState( INITIAL_FRAME_SIZE );
// The width of the resizable frame when a new resize gesture starts. // The width of the resizable frame when a new resize gesture starts.
@ -94,6 +98,27 @@ function ResizableFrame( {
); );
const defaultAspectRatio = defaultSize.width / defaultSize.height; const defaultAspectRatio = defaultSize.width / defaultSize.height;
const deviceStyles = useResizeCanvas( deviceType );
useEffect( () => {
if ( ! deviceType ) {
return;
}
if ( deviceType === 'Desktop' ) {
setFrameSize( INITIAL_FRAME_SIZE );
} else {
const { width, height, marginLeft, marginRight } = deviceStyles;
setIsOversized( width > defaultSize.width );
setFrameSize( {
width: isOversized ? '100%' : width,
height: isOversized ? '100%' : height,
marginLeft,
marginRight,
} );
}
}, [ deviceType ] );
const handleResizeStart = ( _event, _direction, ref ) => { const handleResizeStart = ( _event, _direction, ref ) => {
// Remember the starting width so we don't have to get `ref.offsetWidth` on // Remember the starting width so we don't have to get `ref.offsetWidth` on
// every resize event thereafter, which will cause layout thrashing. // every resize event thereafter, which will cause layout thrashing.
@ -253,7 +278,7 @@ function ResizableFrame( {
right: false, right: false,
bottom: false, bottom: false,
// Resizing will be disabled until the editor content is loaded. // Resizing will be disabled until the editor content is loaded.
left: isReady, left: isReady && isResizingHandleEnabled,
topRight: false, topRight: false,
bottomRight: false, bottomRight: false,
bottomLeft: false, bottomLeft: false,

View File

@ -162,7 +162,7 @@ export const SidebarPatternScreen = ( { category }: { category: string } ) => {
return; return;
} }
const iframe = window.document.querySelector( const iframe = window.document.querySelector(
'.woocommerce-customize-store-assembler > iframe[name="editor-canvas"]' '.woocommerce-customize-store-assembler > .block-editor-iframe__container iframe[name="editor-canvas"]'
) as HTMLIFrameElement; ) as HTMLIFrameElement;
const blockList = iframe?.contentWindow?.document.body.querySelector( const blockList = iframe?.contentWindow?.document.body.querySelector(

View File

@ -523,9 +523,19 @@ body.woocommerce-assembler {
/* Preview Canvas */ /* Preview Canvas */
.edit-site-layout__canvas { .edit-site-layout__canvas {
bottom: 16px; bottom: 16px;
top: 16px; top: 75px;
left: 6px; // the default styles for this undersizes the width by 24px so we want to center this left: 6px; // the default styles for this undersizes the width by 24px so we want to center this
padding: 0 4px 0 16px; padding: 0;
background: #ddd;
border-radius: 0 0 12px 12px;
// Design with AI: This is necessary because the design with AI flow does not have the device toolbar.
.woocommerce-assembler--with-ai & {
top: 16px;
padding: 0 4px 0 16px;
border-radius: 0;
background: transparent;
}
} }
.edit-site-resizable-frame__handle { .edit-site-resizable-frame__handle {
@ -537,13 +547,22 @@ body.woocommerce-assembler {
border-radius: 12px; border-radius: 12px;
/* new frame */ /* new frame */
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.05); box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.05);
transition: all 0.3s ease 0s;
// Design with AI: This is necessary because the design with AI flow does not have the device toolbar.
.woocommerce-assembler--with-ai & {
transition: none;
}
} }
.woocommerce-customize-store__block-editor, .woocommerce-customize-store__block-editor,
.edit-site-layout:not(.is-full-canvas) .edit-site-layout:not(.is-full-canvas)
.edit-site-layout__canvas > div .edit-site-layout__canvas > div
.interface-interface-skeleton__content { .interface-interface-skeleton__content {
border-radius: 12px; // Design with AI: This is necessary because the design with AI flow does not have the device toolbar.
.woocommerce-customize-store__step-designWithAi & {
border-radius: 12px;
}
.woocommerce-customize-store__block-editor, .woocommerce-customize-store__block-editor,
.woocommerce-block-preview-container, .woocommerce-block-preview-container,
@ -560,7 +579,59 @@ body.woocommerce-assembler {
.customize-your-store-edit-site-resizable-frame__inner-content { .customize-your-store-edit-site-resizable-frame__inner-content {
height: 100%; height: 100%;
border-radius: 12px; border-radius: 0 0 12px 12px;
// Design with AI: This is necessary because the design with AI flow does not have the device toolbar.
.woocommerce-customize-store__step-designWithAi & {
border-radius: 12px;
}
}
.woocommerce-customize-store__device-toolbar {
padding: 10px 8px;
text-align: center;
background-color: #fff;
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.05);
position: relative;
top: 16px;
z-index: 10;
border-radius: 12px 12px 0 0;
width: calc(100% - 16px);
left: 6px;
}
.woocommerce-customize-store__device-button:not([disabled]):focus {
box-shadow: none;
outline: none;
}
.woocommerce-customize-store__device-button:not([disabled]) {
&:hover,
&.is-selected {
box-shadow: none;
outline: none;
.woocommerce-customize-store__device-icon {
fill: initial;
&--zoom {
stroke: rgb(0, 0, 0);
}
}
}
}
.woocommerce-customize-store__device-icon {
margin-right: 10px;
fill: #757575;
&--zoom {
stroke: #757575;
}
&:last-of-type {
margin-right: 0;
}
} }
} }

View File

@ -33,6 +33,7 @@ import Delete from './delete';
import './style.scss'; import './style.scss';
import { useIsNoBlocksPlaceholderPresent } from '../hooks/block-placeholder/use-is-no-blocks-placeholder-present'; import { useIsNoBlocksPlaceholderPresent } from '../hooks/block-placeholder/use-is-no-blocks-placeholder-present';
import { SelectedBlockContext } from '../context/selected-block-ref-context'; import { SelectedBlockContext } from '../context/selected-block-ref-context';
import { isFullComposabilityFeatureAndAPIAvailable } from '../utils/is-full-composability-enabled';
const isHomepageUrl = ( path: string ) => { const isHomepageUrl = ( path: string ) => {
return path.includes( '/customize-store/assembler-hub/homepage' ); return path.includes( '/customize-store/assembler-hub/homepage' );
@ -143,6 +144,10 @@ export const Toolbar = () => {
const blockPopoverRef = useRef< HTMLDivElement | null >( null ); const blockPopoverRef = useRef< HTMLDivElement | null >( null );
// Note: This feature is only available when the full composability feature flag is enabled.
const isEligibleForZoomOutFeature =
isFullComposabilityFeatureAndAPIAvailable();
const popoverAnchor = useMemo( () => { const popoverAnchor = useMemo( () => {
if ( ! selectedBlockRef || ! selectedBlockClientId ) { if ( ! selectedBlockRef || ! selectedBlockClientId ) {
return undefined; return undefined;
@ -153,22 +158,39 @@ export const Toolbar = () => {
const { top, width, height } = const { top, width, height } =
selectedBlockRef.getBoundingClientRect(); selectedBlockRef.getBoundingClientRect();
const rect = window.document const iframe = window.document.querySelector(
.querySelector( '.woocommerce-customize-store-assembler > .block-editor-iframe__container iframe[name="editor-canvas"]'
'.woocommerce-customize-store-assembler > iframe[name="editor-canvas"]' );
) const iframeHtmlElement =
?.getBoundingClientRect(); // @ts-expect-error missing type
iframe?.contentDocument?.documentElement;
const iframeRect = iframe?.getBoundingClientRect();
const iframeHtmlElementRect =
iframeHtmlElement?.getBoundingClientRect();
if ( ! rect ) { const isZoomedOut =
isEligibleForZoomOutFeature &&
iframeHtmlElement?.classList.contains( 'is-zoomed-out' );
if ( ! iframeRect ) {
return new window.DOMRect( 0, 0, 0, 0 ); return new window.DOMRect( 0, 0, 0, 0 );
} }
return new window.DOMRect( // Here we need to account for when the iframe is zoomed out as the width changes.
rect?.left + 10, const rectLeft =
Math.max( top + 70 + rect.top, 100 ), isZoomedOut && iframeHtmlElementRect
width, ? iframeRect?.left + iframeHtmlElementRect.left + 60
height : iframeRect?.left + 10;
);
// Here we need to account for when the zoom out feature is eligible because a toolbar is added to the top of the iframe.
const rectTop = isEligibleForZoomOutFeature
? Math.max( top + 70 + iframeRect.top, iframeRect.top + 60 )
: Math.max(
top + 70 + iframeRect.top,
iframeRect.top + 60
);
return new window.DOMRect( rectLeft, rectTop, width, height );
}, },
}; };
}, [ selectedBlockRef, selectedBlockClientId ] ); }, [ selectedBlockRef, selectedBlockClientId ] );

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Adds a Zoom Out feature to the Customize Your Store experience

View File

@ -0,0 +1,4 @@
Significance: patch
Type: add
E2E test coverage for CYS device toolbar

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Fix animation of devicetoolbar

View File

@ -374,4 +374,38 @@ test.describe( 'Assembler -> Full composability', { tag: '@gutenberg' }, () => {
).toHaveCount( 10 ); ).toHaveCount( 10 );
} }
); );
test( 'Clicking buttons in resize and zoom toolbar changes the frame size', async ( {
pageObject,
baseURL,
} ) => {
await prepareAssembler( pageObject, baseURL );
const assembler = await pageObject.getAssembler();
const editor = await pageObject.getEditor();
const toolbar = assembler.locator( '[aria-label="Resize Options"]' );
const resizeContainer = assembler.locator(
'.components-resizable-box__container'
);
const tabletBtn = assembler.locator( '[aria-label="Tablet View"]' );
const mobileBtn = assembler.locator( '[aria-label="Mobile View"]' );
await mobileBtn.click();
const mobileWidth = await resizeContainer.evaluate( ( element ) =>
window.getComputedStyle( element ).getPropertyValue( 'width' )
);
await tabletBtn.click();
const tabletWidth = await resizeContainer.evaluate( ( element ) =>
window.getComputedStyle( element ).getPropertyValue( 'width' )
);
await assembler.locator( '[aria-label="Zoom Out View"]' ).click();
await expect( editor.locator( '.is-zoomed-out' ) ).toBeVisible();
await expect( parseFloat( tabletWidth ) ).toBeGreaterThan(
parseFloat( mobileWidth )
);
await expect( toolbar ).toBeVisible();
} );
} ); } );