From e54a9ec2f97e28db3293628e921e40e81cfe5f76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Mon, 11 Jan 2021 20:07:26 +0200 Subject: [PATCH] FSE: load content in iframe (#25775) --- lib/client-assets.php | 37 ++++ package-lock.json | 1 + packages/block-editor/package.json | 1 + .../components/block-list/block-popover.js | 8 +- .../block-list/block-selection-button.js | 6 + .../src/components/iframe/index.js | 189 ++++++++++++++++++ packages/block-editor/src/components/index.js | 2 + .../src/components/observe-typing/index.js | 100 ++++++--- .../src/components/use-resize-canvas/index.js | 17 +- packages/block-editor/src/style.scss | 2 + packages/components/src/popover/index.js | 109 ++++++++-- packages/e2e-test-utils/README.md | 4 + packages/e2e-test-utils/src/canvas.js | 6 + packages/e2e-test-utils/src/index.js | 1 + packages/e2e-test-utils/src/inserter.js | 12 +- .../e2e-test-utils/src/show-block-toolbar.js | 6 +- .../experiments/multi-entity-editing.test.js | 23 ++- .../experiments/multi-entity-saving.test.js | 3 +- .../specs/experiments/template-part.test.js | 30 +-- .../specs/performance/site-editor.test.js | 7 +- .../src/components/block-editor/index.js | 63 ++++-- .../edit-site/src/components/editor/index.js | 27 +-- .../src/components/editor/style.scss | 9 +- 23 files changed, 535 insertions(+), 128 deletions(-) create mode 100644 packages/block-editor/src/components/iframe/index.js create mode 100644 packages/e2e-test-utils/src/canvas.js diff --git a/lib/client-assets.php b/lib/client-assets.php index 1e978a73ff7a9..54c5f41d4d3a3 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -669,3 +669,40 @@ function gutenberg_extend_block_editor_settings_with_fse_theme_flag( $settings ) return $settings; } add_filter( 'block_editor_settings', 'gutenberg_extend_block_editor_settings_with_fse_theme_flag' ); + +/** + * Sets the editor styles to be consumed by JS. + */ +function gutenberg_extend_block_editor_styles_html() { + $handles = array( + 'wp-block-editor', + 'wp-block-library', + 'wp-edit-blocks', + ); + + $block_registry = WP_Block_Type_Registry::get_instance(); + + foreach ( $block_registry->get_all_registered() as $block_name => $block_type ) { + if ( ! empty( $block_type->style ) ) { + $handles[] = $block_type->style; + } + + if ( ! empty( $block_type->editor_style ) ) { + $handles[] = $block_type->editor_style; + } + } + + $handles = array_unique( $handles ); + $done = wp_styles()->done; + + ob_start(); + + wp_styles()->done = array(); + wp_styles()->do_items( $handles ); + wp_styles()->done = $done; + + $editor_styles = wp_json_encode( array( 'html' => ob_get_clean() ) ); + + echo ""; +} +add_action( 'admin_footer-toplevel_page_gutenberg-edit-site', 'gutenberg_extend_block_editor_styles_html' ); diff --git a/package-lock.json b/package-lock.json index 1ac676306e6f9..cf92071bf6142 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11692,6 +11692,7 @@ "lodash": "^4.17.19", "memize": "^1.1.0", "react-autosize-textarea": "^7.1.0", + "react-merge-refs": "^1.0.0", "react-spring": "^8.0.19", "reakit": "1.3.4", "redux-multi": "^0.1.12", diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index dccbbb8b4772f..4df1e348d459d 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -60,6 +60,7 @@ "lodash": "^4.17.19", "memize": "^1.1.0", "react-autosize-textarea": "^7.1.0", + "react-merge-refs": "^1.0.0", "react-spring": "^8.0.19", "reakit": "1.3.4", "redux-multi": "^0.1.12", diff --git a/packages/block-editor/src/components/block-list/block-popover.js b/packages/block-editor/src/components/block-list/block-popover.js index 5bf1e5a12164d..819765d7913f4 100644 --- a/packages/block-editor/src/components/block-list/block-popover.js +++ b/packages/block-editor/src/components/block-list/block-popover.js @@ -167,7 +167,13 @@ function BlockPopover( { : 'top right left'; const stickyBoundaryElement = showEmptyBlockSideInserter ? undefined - : getScrollContainer( node ) || ownerDocument.body; + : // The sticky boundary element should be the boundary at which the + // the block toolbar becomes sticky when the block scolls out of view. + // In case of an iframe, this should be the iframe boundary, otherwise + // the scroll container. + ownerDocument.defaultView.frameElement || + getScrollContainer( node ) || + ownerDocument.body; return ( { + try { + // May fail for external styles. + // eslint-disable-next-line no-unused-expressions + styleSheet.cssRules; + } catch ( e ) { + return; + } + + const { ownerNode, cssRules } = styleSheet; + + if ( ! cssRules ) { + return; + } + + const isMatch = Array.from( cssRules ).find( + ( { selectorText } ) => + selectorText && + ( selectorText.includes( `.${ BODY_CLASS_NAME }` ) || + selectorText.includes( `.${ BLOCK_PREFIX }` ) ) + ); + + if ( isMatch && ! doc.getElementById( ownerNode.id ) ) { + doc.head.appendChild( ownerNode.cloneNode( true ) ); + } + } ); +} + +/** + * Bubbles some event types (keydown, keypress, and dragover) to parent document + * document to ensure that the keyboard shortcuts and drag and drop work. + * + * Ideally, we should remove event bubbling in the future. Keyboard shortcuts + * should be context dependent, e.g. actions on blocks like Cmd+A should not + * work globally outside the block editor. + * + * @param {Document} doc Document to attach listeners to. + */ +function bubbleEvents( doc ) { + const { defaultView } = doc; + const { frameElement } = defaultView; + + function bubbleEvent( event ) { + const prototype = Object.getPrototypeOf( event ); + const constructorName = prototype.constructor.name; + const Constructor = window[ constructorName ]; + + const init = {}; + + for ( const key in event ) { + init[ key ] = event[ key ]; + } + + if ( event instanceof defaultView.MouseEvent ) { + const rect = frameElement.getBoundingClientRect(); + init.clientX += rect.left; + init.clientY += rect.top; + } + + const newEvent = new Constructor( event.type, init ); + const cancelled = ! frameElement.dispatchEvent( newEvent ); + + if ( cancelled ) { + event.preventDefault(); + } + } + + const eventTypes = [ 'keydown', 'keypress', 'dragover' ]; + + for ( const name of eventTypes ) { + doc.addEventListener( name, bubbleEvent ); + } +} + +/** + * Sets the document direction. + * + * Sets the `editor-styles-wrapper` class name on the body. + * + * Copies the `admin-color-*` class name to the body so that the admin color + * scheme applies to components in the iframe. + * + * @param {Document} doc Document to add class name to. + */ +function setBodyClassName( doc ) { + doc.dir = document.dir; + doc.body.className = BODY_CLASS_NAME; + + for ( const name of document.body.classList ) { + if ( name.startsWith( 'admin-color-' ) ) { + doc.body.classList.add( name ); + } + } +} + +/** + * Sets the document head and default styles. + * + * @param {Document} doc Document to set the head for. + * @param {string} head HTML to set as the head. + */ +function setHead( doc, head ) { + doc.head.innerHTML = + // Body margin must be overridable by themes. + '' + head; +} + +function Iframe( { contentRef, children, head, ...props }, ref ) { + const [ iframeDocument, setIframeDocument ] = useState(); + + const setRef = useCallback( ( node ) => { + if ( ! node ) { + return; + } + + function setDocumentIfReady() { + const { contentDocument } = node; + const { readyState } = contentDocument; + + if ( readyState !== 'interactive' && readyState !== 'complete' ) { + return false; + } + + setIframeDocument( contentDocument ); + setHead( contentDocument, head ); + setBodyClassName( contentDocument ); + styleSheetsCompat( contentDocument ); + bubbleEvents( contentDocument ); + setBodyClassName( contentDocument ); + contentRef.current = contentDocument.body; + + return true; + } + + if ( setDocumentIfReady() ) { + return; + } + + // Document is not immediately loaded in Firefox. + node.addEventListener( 'load', () => { + setDocumentIfReady(); + } ); + }, [] ); + + return ( + + ); +} + +export default forwardRef( Iframe ); diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index 551f83f8b4d66..b944ac97275fa 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -108,6 +108,7 @@ export { default as NavigableToolbar } from './navigable-toolbar'; export { default as ObserveTyping, useTypingObserver as __unstableUseTypingObserver, + useMouseMoveTypingReset as __unstableUseMouseMoveTypingReset, } from './observe-typing'; export { default as PreserveScrollInReorder } from './preserve-scroll-in-reorder'; export { default as SkipToSelectedBlock } from './skip-to-selected-block'; @@ -119,6 +120,7 @@ export { default as Warning } from './warning'; export { default as WritingFlow } from './writing-flow'; export { useCanvasClickRedirect as __unstableUseCanvasClickRedirect } from './use-canvas-click-redirect'; export { default as useBlockDisplayInformation } from './use-block-display-information'; +export { default as __unstableIframe } from './iframe'; /* * State Related Components diff --git a/packages/block-editor/src/components/observe-typing/index.js b/packages/block-editor/src/components/observe-typing/index.js index e33628c92b374..3476d62c9692b 100644 --- a/packages/block-editor/src/components/observe-typing/index.js +++ b/packages/block-editor/src/components/observe-typing/index.js @@ -15,6 +15,8 @@ import { TAB, } from '@wordpress/keycodes'; +/** @typedef {import('@wordpress/element').RefObject} RefObject */ + /** * Set of key codes upon which typing is to be initiated on a keydown event. * @@ -43,12 +45,77 @@ function isKeyDownEligibleForStartTyping( event ) { return ! shiftKey && KEY_DOWN_ELIGIBLE_KEY_CODES.has( keyCode ); } +/** + * Removes the `isTyping` flag when the mouse moves in the document of the given + * element. + * + * @param {RefObject} ref React ref containing an element. + */ +export function useMouseMoveTypingReset( ref ) { + const isTyping = useSelect( ( select ) => + select( 'core/block-editor' ).isTyping() + ); + const { stopTyping } = useDispatch( 'core/block-editor' ); + + useEffect( () => { + if ( ! isTyping ) { + return; + } + + const element = ref.current; + const { ownerDocument } = element; + let lastClientX; + let lastClientY; + + /** + * On mouse move, unset typing flag if user has moved cursor. + * + * @param {MouseEvent} event Mousemove event. + */ + function stopTypingOnMouseMove( event ) { + const { clientX, clientY } = event; + + // We need to check that the mouse really moved because Safari + // triggers mousemove events when shift or ctrl are pressed. + if ( + lastClientX && + lastClientY && + ( lastClientX !== clientX || lastClientY !== clientY ) + ) { + stopTyping(); + } + + lastClientX = clientX; + lastClientY = clientY; + } + + ownerDocument.addEventListener( 'mousemove', stopTypingOnMouseMove ); + + return () => { + ownerDocument.removeEventListener( + 'mousemove', + stopTypingOnMouseMove + ); + }; + }, [ isTyping, stopTyping ] ); +} + +/** + * Sets and removes the `isTyping` flag based on user actions: + * + * - Sets the flag if the user types within the given element. + * - Removes the flag when the user selects some text, focusses a non-text + * field, presses ESC or TAB, or moves the mouse in the document. + * + * @param {RefObject} ref React ref containing an element. + */ export function useTypingObserver( ref ) { const isTyping = useSelect( ( select ) => select( 'core/block-editor' ).isTyping() ); const { startTyping, stopTyping } = useDispatch( 'core/block-editor' ); + useMouseMoveTypingReset( ref ); useEffect( () => { const element = ref.current; const { ownerDocument } = element; @@ -108,41 +175,12 @@ export function useTypingObserver( ref ) { } } - let lastClientX; - let lastClientY; - - /** - * On mouse move, unset typing flag if user has moved cursor. - * - * @param {MouseEvent} event Mousemove event. - */ - function stopTypingOnMouseMove( event ) { - const { clientX, clientY } = event; - - // We need to check that the mouse really moved because Safari - // triggers mousemove events when shift or ctrl are pressed. - if ( - lastClientX && - lastClientY && - ( lastClientX !== clientX || lastClientY !== clientY ) - ) { - stopTyping(); - } - - lastClientX = clientX; - lastClientY = clientY; - } - element.addEventListener( 'focus', stopTypingOnNonTextField ); element.addEventListener( 'keydown', stopTypingOnEscapeKey ); ownerDocument.addEventListener( 'selectionchange', stopTypingOnSelectionUncollapse ); - ownerDocument.addEventListener( - 'mousemove', - stopTypingOnMouseMove - ); return () => { defaultView.clearTimeout( timerId ); @@ -155,10 +193,6 @@ export function useTypingObserver( ref ) { 'selectionchange', stopTypingOnSelectionUncollapse ); - ownerDocument.removeEventListener( - 'mousemove', - stopTypingOnMouseMove - ); }; } diff --git a/packages/block-editor/src/components/use-resize-canvas/index.js b/packages/block-editor/src/components/use-resize-canvas/index.js index d5134d634aabd..bd55ab5d6b3b2 100644 --- a/packages/block-editor/src/components/use-resize-canvas/index.js +++ b/packages/block-editor/src/components/use-resize-canvas/index.js @@ -11,11 +11,15 @@ import { default as useSimulatedMediaQuery } from '../../components/use-simulate /** * Function to resize the editor window. * - * @param {string} deviceType Used for determining the size of the container (e.g. Desktop, Tablet, Mobile) + * @param {string} deviceType Used for determining the size of the container (e.g. Desktop, Tablet, Mobile) + * @param {boolean} __unstableDisableSimulation Whether to disable media query simulation. * * @return {Object} Inline styles to be added to resizable container. */ -export default function useResizeCanvas( deviceType ) { +export default function useResizeCanvas( + deviceType, + __unstableDisableSimulation +) { const [ actualWidth, updateActualWidth ] = useState( window.innerWidth ); useEffect( () => { @@ -69,10 +73,11 @@ export default function useResizeCanvas( deviceType ) { } }; - useSimulatedMediaQuery( - 'resizable-editor-section', - getCanvasWidth( deviceType ) - ); + const width = __unstableDisableSimulation + ? null + : getCanvasWidth( deviceType ); + + useSimulatedMediaQuery( 'resizable-editor-section', width ); return contentInlineStyles( deviceType ); } diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index 1ea21ed457452..e4201596a11f1 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -64,3 +64,5 @@ @import "./components/block-toolbar/style.scss"; @import "./components/inserter/style.scss"; @import "./components/preview-options/style.scss"; + +@include wordpress-admin-schemes(); diff --git a/packages/components/src/popover/index.js b/packages/components/src/popover/index.js index 32382b21f2231..90cee30f2c81b 100644 --- a/packages/components/src/popover/index.js +++ b/packages/components/src/popover/index.js @@ -42,6 +42,24 @@ import { getAnimateClassName } from '../animate'; */ const SLOT_NAME = 'Popover'; +function offsetIframe( rect, ownerDocument ) { + const { defaultView } = ownerDocument; + const { frameElement } = defaultView; + + if ( ! frameElement ) { + return rect; + } + + const iframeRect = frameElement.getBoundingClientRect(); + + return new defaultView.DOMRect( + rect.left + iframeRect.left, + rect.top + iframeRect.top, + rect.width, + rect.height + ); +} + function computeAnchorRect( anchorRefFallback, anchorRect, @@ -75,14 +93,20 @@ function computeAnchorRect( // `anchorRef instanceof window.Range` checks will break across document boundaries // such as in an iframe if ( typeof anchorRef?.cloneRange === 'function' ) { - return getRectangleFromRange( anchorRef ); + return offsetIframe( + getRectangleFromRange( anchorRef ), + anchorRef.endContainer.ownerDocument + ); } // Duck-type to check if `anchorRef` is an instance of Element // `anchorRef instanceof window.Element` checks will break across document boundaries // such as in an iframe if ( typeof anchorRef?.getBoundingClientRect === 'function' ) { - const rect = anchorRef.getBoundingClientRect(); + const rect = offsetIframe( + anchorRef.getBoundingClientRect(), + anchorRef.ownerDocument + ); if ( shouldAnchorIncludePadding ) { return rect; @@ -94,11 +118,14 @@ function computeAnchorRect( const { top, bottom } = anchorRef; const topRect = top.getBoundingClientRect(); const bottomRect = bottom.getBoundingClientRect(); - const rect = new window.DOMRect( - topRect.left, - topRect.top, - topRect.width, - bottomRect.bottom - topRect.top + const rect = offsetIframe( + new window.DOMRect( + topRect.left, + topRect.top, + topRect.width, + bottomRect.bottom - topRect.top + ), + top.ownerDocument ); if ( shouldAnchorIncludePadding ) { @@ -199,6 +226,22 @@ function setClass( element, name, toggle ) { } } +function getAnchorDocument( anchor ) { + if ( ! anchor ) { + return; + } + + if ( anchor.endContainer ) { + return anchor.endContainer.ownerDocument; + } + + if ( anchor.top ) { + return anchor.top.ownerDocument; + } + + return anchor.ownerDocument; +} + const Popover = ( { headerTitle, onClose, @@ -363,6 +406,9 @@ const Popover = ( { refresh(); + const { ownerDocument } = containerRef.current; + const { defaultView } = ownerDocument; + /* * There are sometimes we need to reposition or resize the popover that * are not handled by the resize/scroll window events (i.e. CSS changes @@ -370,35 +416,60 @@ const Popover = ( { * * For these situations, we refresh the popover every 0.5s */ - const intervalHandle = window.setInterval( refresh, 500 ); + const intervalHandle = defaultView.setInterval( refresh, 500 ); let rafId; const refreshOnAnimationFrame = () => { - window.cancelAnimationFrame( rafId ); - rafId = window.requestAnimationFrame( refresh ); + defaultView.cancelAnimationFrame( rafId ); + rafId = defaultView.requestAnimationFrame( refresh ); }; // Sometimes a click trigger a layout change that affects the popover // position. This is an opportunity to immediately refresh rather than // at the interval. - window.addEventListener( 'click', refreshOnAnimationFrame ); - window.addEventListener( 'resize', refresh ); - window.addEventListener( 'scroll', refresh, true ); + defaultView.addEventListener( 'click', refreshOnAnimationFrame ); + defaultView.addEventListener( 'resize', refresh ); + defaultView.addEventListener( 'scroll', refresh, true ); + + const anchorDocument = getAnchorDocument( anchorRef ); + + // If the anchor is within an iframe, the popover position also needs + // to refrest when the iframe content is scrolled or resized. + if ( anchorDocument && anchorDocument !== ownerDocument ) { + anchorDocument.defaultView.addEventListener( 'resize', refresh ); + anchorDocument.defaultView.addEventListener( + 'scroll', + refresh, + true + ); + } let observer; if ( __unstableObserveElement ) { - observer = new window.MutationObserver( refresh ); + observer = new defaultView.MutationObserver( refresh ); observer.observe( __unstableObserveElement, { attributes: true } ); } return () => { - window.clearInterval( intervalHandle ); - window.removeEventListener( 'resize', refresh ); - window.removeEventListener( 'scroll', refresh, true ); - window.removeEventListener( 'click', refreshOnAnimationFrame ); - window.cancelAnimationFrame( rafId ); + defaultView.clearInterval( intervalHandle ); + defaultView.removeEventListener( 'resize', refresh ); + defaultView.removeEventListener( 'scroll', refresh, true ); + defaultView.removeEventListener( 'click', refreshOnAnimationFrame ); + defaultView.cancelAnimationFrame( rafId ); + + if ( anchorDocument && anchorDocument !== ownerDocument ) { + anchorDocument.defaultView.removeEventListener( + 'resize', + refresh + ); + anchorDocument.defaultView.removeEventListener( + 'scroll', + refresh, + true + ); + } if ( observer ) { observer.disconnect(); diff --git a/packages/e2e-test-utils/README.md b/packages/e2e-test-utils/README.md index 9ff42915efb69..cbd0e58929b74 100644 --- a/packages/e2e-test-utils/README.md +++ b/packages/e2e-test-utils/README.md @@ -42,6 +42,10 @@ _Returns_ - `Promise`: Boolean which represents the state of prepublish checks. +# **canvas** + +Gets the editor canvas frame. + # **changeSiteTimezone** Visits general settings page and changes the timezone to the given value. diff --git a/packages/e2e-test-utils/src/canvas.js b/packages/e2e-test-utils/src/canvas.js new file mode 100644 index 0000000000000..965007d432d0e --- /dev/null +++ b/packages/e2e-test-utils/src/canvas.js @@ -0,0 +1,6 @@ +/** + * Gets the editor canvas frame. + */ +export function canvas() { + return page.frames().find( ( f ) => f.name() === 'editor-canvas' ) || page; +} diff --git a/packages/e2e-test-utils/src/index.js b/packages/e2e-test-utils/src/index.js index c70f7fd15f69c..06899b154c983 100644 --- a/packages/e2e-test-utils/src/index.js +++ b/packages/e2e-test-utils/src/index.js @@ -2,6 +2,7 @@ export { activatePlugin } from './activate-plugin'; export { activateTheme } from './activate-theme'; export { arePrePublishChecksEnabled } from './are-pre-publish-checks-enabled'; export { changeSiteTimezone } from './change-site-timezone'; +export { canvas } from './canvas'; export { clearLocalStorage } from './clear-local-storage'; export { clickBlockAppender } from './click-block-appender'; export { clickBlockToolbarButton } from './click-block-toolbar-button'; diff --git a/packages/e2e-test-utils/src/inserter.js b/packages/e2e-test-utils/src/inserter.js index 800148d2c39e1..0b58f730f9bfb 100644 --- a/packages/e2e-test-utils/src/inserter.js +++ b/packages/e2e-test-utils/src/inserter.js @@ -2,6 +2,7 @@ * Internal dependencies */ import { pressKeyWithModifier } from './press-key-with-modifier'; +import { canvas } from './canvas'; // This selector is written to support the current and old inserter markup // because the performance tests need to be able to run across versions. @@ -57,12 +58,11 @@ export async function toggleGlobalBlockInserter() { * Retrieves the document container by css class and checks to make sure the document's active element is within it */ async function waitForInserterCloseAndContentFocus() { - await page.waitForFunction( () => - document.body - .querySelector( - '.interface-interface-skeleton__content .block-editor-block-list__layout' - ) - .contains( document.activeElement ) + await canvas().waitForFunction( + () => + document.activeElement.closest( + '.block-editor-block-list__layout' + ) !== null ); } diff --git a/packages/e2e-test-utils/src/show-block-toolbar.js b/packages/e2e-test-utils/src/show-block-toolbar.js index f1ac720e028d4..3d91232f8800e 100644 --- a/packages/e2e-test-utils/src/show-block-toolbar.js +++ b/packages/e2e-test-utils/src/show-block-toolbar.js @@ -3,7 +3,11 @@ * Call this function to reveal it. */ export async function showBlockToolbar() { - // Move the mouse to disable the isTyping mode + // Move the mouse to disable the isTyping mode. We need at least three + // mousemove events for it to work across windows (iframe). With three + // moves, it's a guarantee that at least two will be in the same window. + // Two events are required for the flag to be unset. await page.mouse.move( 50, 50 ); + await page.mouse.move( 75, 75 ); await page.mouse.move( 100, 100 ); } diff --git a/packages/e2e-tests/specs/experiments/multi-entity-editing.test.js b/packages/e2e-tests/specs/experiments/multi-entity-editing.test.js index be02c89a1eb8f..0369fc8463ce5 100644 --- a/packages/e2e-tests/specs/experiments/multi-entity-editing.test.js +++ b/packages/e2e-tests/specs/experiments/multi-entity-editing.test.js @@ -8,6 +8,7 @@ import { publishPost, trashAllPosts, activateTheme, + canvas, } from '@wordpress/e2e-test-utils'; import { addQueryArgs } from '@wordpress/url'; @@ -21,7 +22,7 @@ const visitSiteEditor = async () => { page: 'gutenberg-edit-site', } ).slice( 1 ); await visitAdminPage( 'admin.php', query ); - await page.waitForSelector( '.edit-site-visual-editor' ); + await page.waitForSelector( '.edit-site-visual-editor iframe' ); }; const clickTemplateItem = async ( menus, itemName ) => { @@ -144,15 +145,25 @@ describe( 'Multi-entity editor states', () => { it( 'should not dirty an entity by switching to it in the template dropdown', async () => { await visitSiteEditor(); await clickTemplateItem( 'Template Parts', 'header' ); + await page.waitForFunction( () => + Array.from( window.frames ).find( + ( { name } ) => name === 'editor-canvas' + ) + ); // Wait for blocks to load. - await page.waitForSelector( '.wp-block' ); + await canvas().waitForSelector( '.wp-block' ); expect( await isEntityDirty( 'header' ) ).toBe( false ); expect( await isEntityDirty( 'front-page' ) ).toBe( false ); // Switch back and make sure it is still clean. await clickTemplateItem( 'Templates', 'Front Page' ); - await page.waitForSelector( '.wp-block' ); + await page.waitForFunction( () => + Array.from( window.frames ).find( + ( { name } ) => name === 'editor-canvas' + ) + ); + await canvas().waitForSelector( '.wp-block' ); expect( await isEntityDirty( 'header' ) ).toBe( false ); expect( await isEntityDirty( 'front-page' ) ).toBe( false ); @@ -186,7 +197,7 @@ describe( 'Multi-entity editor states', () => { await visitSiteEditor(); // Wait for site editor to load. - await page.waitForSelector( + await canvas().waitForSelector( '.wp-block-template-part .block-editor-block-list__layout' ); @@ -237,7 +248,7 @@ describe( 'Multi-entity editor states', () => { } ); it( 'should only dirty the child when editing the child', async () => { - await page.click( + await canvas().click( '.wp-block-template-part .wp-block[data-type="core/paragraph"]' ); await page.keyboard.type( 'Some more test words!' ); @@ -248,7 +259,7 @@ describe( 'Multi-entity editor states', () => { } ); it( 'should only dirty the nested entity when editing the nested entity', async () => { - await page.click( + await canvas().click( '.wp-block-template-part .wp-block-template-part .wp-block[data-type="core/paragraph"]' ); await page.keyboard.type( 'Nested test words!' ); diff --git a/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js b/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js index 7e2a1e127be5f..c4df3c928f7bd 100644 --- a/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js +++ b/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js @@ -8,6 +8,7 @@ import { visitAdminPage, trashAllPosts, activateTheme, + canvas, } from '@wordpress/e2e-test-utils'; import { addQueryArgs } from '@wordpress/url'; @@ -198,7 +199,7 @@ describe( 'Multi-entity save flow', () => { await navigationPanel.close(); // Click the first block so that the template part inserts in the right place. - const firstBlock = await page.$( '.wp-block' ); + const firstBlock = await canvas().$( '.wp-block' ); await firstBlock.click(); // Insert something to dirty the editor. diff --git a/packages/e2e-tests/specs/experiments/template-part.test.js b/packages/e2e-tests/specs/experiments/template-part.test.js index 112f6bfc59681..3fd59b6db0ef2 100644 --- a/packages/e2e-tests/specs/experiments/template-part.test.js +++ b/packages/e2e-tests/specs/experiments/template-part.test.js @@ -11,6 +11,7 @@ import { getAllBlocks, selectBlockByClientId, clickBlockToolbarButton, + canvas, } from '@wordpress/e2e-test-utils'; import { addQueryArgs } from '@wordpress/url'; @@ -39,7 +40,7 @@ describe( 'Template Part', () => { page: 'gutenberg-edit-site', } ).slice( 1 ) ); - await page.waitForSelector( '.edit-site-visual-editor' ); + await page.waitForSelector( '.edit-site-visual-editor iframe' ); } ); async function updateHeader( content ) { @@ -89,7 +90,7 @@ describe( 'Template Part', () => { } async function assertParagraphInTemplatePart( content ) { - const paragraphInTemplatePart = await page.waitForXPath( + const paragraphInTemplatePart = await canvas().waitForXPath( `//*[@data-type="core/template-part"][//p[text()="${ content }"]]` ); expect( paragraphInTemplatePart ).not.toBeNull(); @@ -105,7 +106,7 @@ describe( 'Template Part', () => { it( 'Should detach blocks from template part', async () => { await updateHeader( 'Header Template Part 456' ); - const initialTemplateParts = await page.$$( + const initialTemplateParts = await canvas().$$( '.wp-block-template-part' ); @@ -115,16 +116,17 @@ describe( 'Template Part', () => { ( block ) => block.name === 'core/template-part' ); await selectBlockByClientId( headerBlock.clientId ); + + // Detach blocks from template part using ellipsis menu. + await triggerEllipsisMenuItem( 'Detach blocks from template part' ); + // TODO: Remove when toolbar supports text fields expect( console ).toHaveWarnedWith( 'Using custom components as toolbar controls is deprecated. Please use ToolbarItem or ToolbarButton components instead. See: https://developer.wordpress.org/block-editor/components/toolbar-button/#inside-blockcontrols' ); - // Detach blocks from template part using ellipsis menu. - await triggerEllipsisMenuItem( 'Detach blocks from template part' ); - // Verify there is one less template part on the page. - const finalTemplateParts = await page.$$( + const finalTemplateParts = await canvas().$$( '.wp-block-template-part' ); expect( @@ -132,15 +134,15 @@ describe( 'Template Part', () => { ).toBe( 1 ); // Verify content of the template part is still present. - const [ expectedContent ] = await page.$x( + const [ expectedContent ] = await canvas().$x( '//p[contains(text(), "Header Template Part 456")]' ); expect( expectedContent ).not.toBeUndefined(); } ); it( 'Should convert selected block to template part', async () => { - await page.waitForSelector( '.wp-block-template-part' ); - const initialTemplateParts = await page.$$( + await canvas().waitForSelector( '.wp-block-template-part' ); + const initialTemplateParts = await canvas().$$( '.wp-block-template-part' ); @@ -162,7 +164,7 @@ describe( 'Template Part', () => { ); // Verify there is 1 more template part on the page than previously. - const finalTemplateParts = await page.$$( + const finalTemplateParts = await canvas().$$( '.wp-block-template-part' ); expect( @@ -171,8 +173,8 @@ describe( 'Template Part', () => { } ); it( 'Should convert multiple selected blocks to template part', async () => { - await page.waitForSelector( '.wp-block-template-part' ); - const initialTemplateParts = await page.$$( + await canvas().waitForSelector( '.wp-block-template-part' ); + const initialTemplateParts = await canvas().$$( '.wp-block-template-part' ); @@ -206,7 +208,7 @@ describe( 'Template Part', () => { ); // Verify there is 1 more template part on the page than previously. - const finalTemplateParts = await page.$$( + const finalTemplateParts = await canvas().$$( '.wp-block-template-part' ); expect( diff --git a/packages/e2e-tests/specs/performance/site-editor.test.js b/packages/e2e-tests/specs/performance/site-editor.test.js index b638c1db1c302..894c285285268 100644 --- a/packages/e2e-tests/specs/performance/site-editor.test.js +++ b/packages/e2e-tests/specs/performance/site-editor.test.js @@ -11,6 +11,7 @@ import { trashAllPosts, visitAdminPage, activateTheme, + canvas, } from '@wordpress/e2e-test-utils'; import { addQueryArgs } from '@wordpress/url'; @@ -51,7 +52,11 @@ describe( 'Site Editor Performance', () => { while ( i-- ) { const startTime = new Date(); await page.reload(); - await page.waitForSelector( '.wp-block', { timeout: 120000 } ); + await page.waitForSelector( '.edit-site-visual-editor', { + timeout: 120000, + } ); + await canvas().waitForSelector( '.wp-block', { timeout: 120000 } ); + results.load.push( new Date() - startTime ); } diff --git a/packages/edit-site/src/components/block-editor/index.js b/packages/edit-site/src/components/block-editor/index.js index aff93bd440ca0..d636705084bcb 100644 --- a/packages/edit-site/src/components/block-editor/index.js +++ b/packages/edit-site/src/components/block-editor/index.js @@ -10,10 +10,15 @@ import { __experimentalLinkControl, BlockInspector, WritingFlow, - ObserveTyping, BlockList, + __experimentalUseResizeCanvas as useResizeCanvas, __unstableUseBlockSelectionClearer as useBlockSelectionClearer, + __unstableUseTypingObserver as useTypingObserver, + __unstableUseMouseMoveTypingReset as useMouseMoveTypingReset, + __unstableUseEditorStyles as useEditorStyles, + __unstableIframe as Iframe, } from '@wordpress/block-editor'; +import { DropZoneProvider, Popover } from '@wordpress/components'; /** * Internal dependencies @@ -22,16 +27,34 @@ import TemplatePartConverter from '../template-part-converter'; import NavigateToLink from '../navigate-to-link'; import { SidebarInspectorFill } from '../sidebar'; +function Canvas( { body, styles } ) { + useBlockSelectionClearer( body ); + useTypingObserver( body ); + useEditorStyles( body, styles ); + + return ( + + + + + + ); +} + export default function BlockEditor( { setIsInserterOpen } ) { - const { settings, templateType, page } = useSelect( + const { settings, templateType, page, deviceType } = useSelect( ( select ) => { - const { getSettings, getEditedPostType, getPage } = select( - 'core/edit-site' - ); + const { + getSettings, + getEditedPostType, + getPage, + __experimentalGetPreviewDeviceType, + } = select( 'core/edit-site' ); return { settings: getSettings( setIsInserterOpen ), templateType: getEditedPostType(), page: getPage(), + deviceType: __experimentalGetPreviewDeviceType(), }; }, [ setIsInserterOpen ] @@ -41,9 +64,18 @@ export default function BlockEditor( { setIsInserterOpen } ) { templateType ); const { setPage } = useDispatch( 'core/edit-site' ); + + const resizedCanvasStyles = useResizeCanvas( deviceType, true ); const ref = useRef(); + const contentRef = useRef(); + + useMouseMoveTypingReset( ref ); - useBlockSelectionClearer( ref ); + // Allow scrolling "through" popovers over the canvas. This is only called + // for as long as the pointer is over a popover. + function onWheel( { deltaX, deltaY } ) { + contentRef.current.scrollBy( deltaX, deltaY ); + } return ( -
- - - - - +
+ +
); diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index dc5818e3132c8..6bf6e0751a356 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -1,13 +1,7 @@ /** * WordPress dependencies */ -import { - useEffect, - useState, - useMemo, - useCallback, - useRef, -} from '@wordpress/element'; +import { useEffect, useState, useMemo, useCallback } from '@wordpress/element'; import { useSelect, useDispatch } from '@wordpress/data'; import { SlotFillProvider, @@ -19,8 +13,6 @@ import { EntityProvider } from '@wordpress/core-data'; import { BlockContextProvider, BlockBreadcrumb, - __unstableUseEditorStyles as useEditorStyles, - __experimentalUseResizeCanvas as useResizeCanvas, __experimentalLibrary as Library, } from '@wordpress/block-editor'; import { @@ -59,7 +51,6 @@ function Editor() { const { isFullscreenActive, isInserterOpen, - deviceType, sidebarIsOpened, settings, entityId, @@ -71,7 +62,6 @@ function Editor() { const { isFeatureActive, isInserterOpened, - __experimentalGetPreviewDeviceType, getSettings, getEditedPostType, getEditedPostId, @@ -85,7 +75,6 @@ function Editor() { return { isInserterOpen: isInserterOpened(), isFullscreenActive: isFeatureActive( 'fullscreenMode' ), - deviceType: __experimentalGetPreviewDeviceType(), sidebarIsOpened: !! select( interfaceStore ).getActiveComplementaryArea( 'core/edit-site' ), @@ -115,8 +104,6 @@ function Editor() { updateEditorSettings( { defaultTemplateTypes } ); }, [ defaultTemplateTypes ] ); - const inlineStyles = useResizeCanvas( deviceType ); - const [ isEntitiesSavedStatesOpen, setIsEntitiesSavedStatesOpen, @@ -163,9 +150,6 @@ function Editor() { }, [ isNavigationOpen ] ); const isMobile = useViewportMatch( 'medium', '<' ); - const ref = useRef(); - - useEditorStyles( ref, settings.styles ); const [ inserterDialogRef, inserterDialogProps ] = useDialog( { onClose: () => setIsInserterOpened( false ), @@ -200,7 +184,6 @@ function Editor() { } secondarySidebar={ @@ -252,12 +235,8 @@ function Editor() { /> } content={ -
+ <> - { template && ( ) } -
+ } actions={ <> diff --git a/packages/edit-site/src/components/editor/style.scss b/packages/edit-site/src/components/editor/style.scss index cb8048a51cd5c..b400e93cce2a9 100644 --- a/packages/edit-site/src/components/editor/style.scss +++ b/packages/edit-site/src/components/editor/style.scss @@ -22,7 +22,14 @@ .edit-site-visual-editor { position: relative; - background-color: $white; + height: 100%; + + iframe { + display: block; + width: 100%; + height: 100%; + background-color: $white; + } } .edit-site-editor__inserter-panel {