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;
} ) => {
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;
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
// 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
@ -11,6 +13,8 @@ import {
forwardRef,
useMemo,
useEffect,
useRef,
useContext,
} from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import {
@ -21,37 +25,42 @@ import {
} from '@wordpress/compose';
import { __experimentalStyleProvider as StyleProvider } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor';
/**
* Internal dependencies
*/
import { ZoomOutContext } from './context/zoom-out-context';
function Iframe( {
contentRef,
children,
tabIndex = 0,
scale = 1,
frameSize = 0,
expand = false,
readonly,
forwardedRef: ref,
loadStyles = true,
loadScripts = false,
title = __( 'Editor canvas', 'woocommerce' ),
canEnableZoomOutView = false,
...props
} ) {
const [ iframeDocument, setIframeDocument ] = useState();
const [ bodyClasses, setBodyClasses ] = useState( [] );
const { resolvedAssets } = useSelect( ( select ) => {
const settings = select( blockEditorStore ).getSettings();
const { getSettings } = select( blockEditorStore );
const settings = getSettings();
return {
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 } ] =
useResizeObserver();
const [ containerResizeListener, { width: containerWidth } ] =
useResizeObserver();
const setRef = useRefEffect( ( node ) => {
node._load = () => {
setIframeDocument( node.contentDocument );
@ -62,6 +71,9 @@ function Iframe( {
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(
Array.from( ownerDocument?.body.classList ).filter(
( name ) =>
@ -70,6 +82,8 @@ function Iframe( {
name === 'wp-embed-responsive'
)
);
contentDocument.dir = ownerDocument.dir;
}
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 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
// mode. Also preload the styles to avoid a flash of unstyled
@ -89,10 +148,23 @@ function Iframe( {
const html = `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script>window.frameElement._load()</script>
<style>html{height:auto!important;min-height:100%;}body{margin:0}</style>
${ loadStyles ? styles : '' }
${ loadScripts ? scripts : '' }
<style>
html{
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>
<body>
<script>document.currentScript.parentElement.remove()</script>
@ -108,30 +180,86 @@ function Iframe( {
useEffect( () => cleanup, [ cleanup ] );
// We need to counter the margin created by scaling the iframe. If the scale
// is e.g. 0.45, then the top + bottom margin is 0.55 (1 - scale). Just the
// top or bottom margin is 0.55 / 2 ((1 - scale) / 2).
const marginFromScaling = ( contentHeight * ( 1 - scale ) ) / 2;
useEffect( () => {
if ( ! canEnableZoomOutView || ! iframeDocument || ! isZoomedOut ) {
return;
}
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
{ ...props }
style={ {
border: 0,
...props.style,
height: expand ? contentHeight : 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,
height: props.style?.height,
transition: 'all .3s',
} }
ref={ useMergeRefs( [ ref, setRef ] ) }
@ -140,7 +268,7 @@ function Iframe( {
// mode. Also preload the styles to avoid a flash of unstyled
// content.
src={ src }
title={ __( 'Editor canvas', 'woocommerce' ) }
title={ title }
name="editor-canvas"
>
{ iframeDocument &&
@ -163,6 +291,26 @@ function 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 ) {
@ -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.
// 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
@ -181,7 +338,12 @@ function IframeIfReady( props, ref ) {
return null;
}
return <Iframe { ...props } forwardedRef={ ref } />;
const iframeProps = {
...props,
...zoomOutProps,
};
return <Iframe { ...iframeProps } forwardedRef={ ref } />;
}
export default forwardRef( IframeIfReady );

View File

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

View File

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

View File

@ -5,7 +5,7 @@
* External dependencies
*/
import clsx from 'clsx';
import { useState, useRef, createContext } from '@wordpress/element';
import { useState, useRef, createContext, useEffect } from '@wordpress/element';
import {
ResizableBox,
Tooltip,
@ -14,6 +14,7 @@ import {
} from '@wordpress/components';
import { useInstanceId } from '@wordpress/compose';
import { __ } from '@wordpress/i18n';
import { __experimentalUseResizeCanvas as useResizeCanvas } from '@wordpress/block-editor';
// Removes the inline styles in the drag handles.
const HANDLE_STYLES_OVERRIDE = {
@ -78,6 +79,9 @@ function ResizableFrame( {
defaultSize,
innerContentStyle,
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 );
// 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 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 ) => {
// Remember the starting width so we don't have to get `ref.offsetWidth` on
// every resize event thereafter, which will cause layout thrashing.
@ -253,7 +278,7 @@ function ResizableFrame( {
right: false,
bottom: false,
// Resizing will be disabled until the editor content is loaded.
left: isReady,
left: isReady && isResizingHandleEnabled,
topRight: false,
bottomRight: false,
bottomLeft: false,

View File

@ -162,7 +162,7 @@ export const SidebarPatternScreen = ( { category }: { category: string } ) => {
return;
}
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;
const blockList = iframe?.contentWindow?.document.body.querySelector(

View File

@ -523,9 +523,19 @@ body.woocommerce-assembler {
/* Preview Canvas */
.edit-site-layout__canvas {
bottom: 16px;
top: 16px;
top: 75px;
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 {
@ -537,13 +547,22 @@ body.woocommerce-assembler {
border-radius: 12px;
/* new frame */
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,
.edit-site-layout:not(.is-full-canvas)
.edit-site-layout__canvas > div
.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-block-preview-container,
@ -560,7 +579,59 @@ body.woocommerce-assembler {
.customize-your-store-edit-site-resizable-frame__inner-content {
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 { useIsNoBlocksPlaceholderPresent } from '../hooks/block-placeholder/use-is-no-blocks-placeholder-present';
import { SelectedBlockContext } from '../context/selected-block-ref-context';
import { isFullComposabilityFeatureAndAPIAvailable } from '../utils/is-full-composability-enabled';
const isHomepageUrl = ( path: string ) => {
return path.includes( '/customize-store/assembler-hub/homepage' );
@ -143,6 +144,10 @@ export const Toolbar = () => {
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( () => {
if ( ! selectedBlockRef || ! selectedBlockClientId ) {
return undefined;
@ -153,22 +158,39 @@ export const Toolbar = () => {
const { top, width, height } =
selectedBlockRef.getBoundingClientRect();
const rect = window.document
.querySelector(
'.woocommerce-customize-store-assembler > iframe[name="editor-canvas"]'
)
?.getBoundingClientRect();
const iframe = window.document.querySelector(
'.woocommerce-customize-store-assembler > .block-editor-iframe__container iframe[name="editor-canvas"]'
);
const iframeHtmlElement =
// @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(
rect?.left + 10,
Math.max( top + 70 + rect.top, 100 ),
width,
height
);
// Here we need to account for when the iframe is zoomed out as the width changes.
const rectLeft =
isZoomedOut && iframeHtmlElementRect
? iframeRect?.left + iframeHtmlElementRect.left + 60
: 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 ] );

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 );
}
);
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();
} );
} );