Revert "CYS: Revert Zoom Feature (#50535)"
This reverts commit 16ef869587
.
This commit is contained in:
parent
0f7773dd47
commit
4ff24a0188
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 ] );
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Adds a Zoom Out feature to the Customize Your Store experience
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: add
|
||||
|
||||
E2E test coverage for CYS device toolbar
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Fix animation of devicetoolbar
|
|
@ -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();
|
||||
} );
|
||||
} );
|
||||
|
|
Loading…
Reference in New Issue