diff --git a/packages/block-editor/src/hooks/use-zoom-out.js b/packages/block-editor/src/hooks/use-zoom-out.js index 584dc55f152cb..a51fca1268299 100644 --- a/packages/block-editor/src/hooks/use-zoom-out.js +++ b/packages/block-editor/src/hooks/use-zoom-out.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { useSelect, useDispatch } from '@wordpress/data'; -import { useEffect } from '@wordpress/element'; +import { useEffect, useRef } from '@wordpress/element'; /** * Internal dependencies @@ -21,16 +21,43 @@ export function useZoomOut( enabled = true ) { ); const { isZoomOut } = unlock( useSelect( blockEditorStore ) ); + const isZoomedOut = isZoomOut(); + // If starting from zoom out engaged, do not control zoom level for the user. + const controlZoomLevel = useRef( ! isZoomedOut ); + useEffect( () => { - if ( ! enabled ) { + if ( ! enabled || ! controlZoomLevel.current ) { return; } - const isAlreadyInZoomOut = isZoomOut(); if ( ! isAlreadyInZoomOut ) { setZoomLevel( 'auto-scaled' ); } - return () => ! isAlreadyInZoomOut && resetZoomLevel(); + return () => { + return ( + controlZoomLevel.current && + ! isAlreadyInZoomOut && + resetZoomLevel() + ); + }; }, [ enabled, isZoomOut, resetZoomLevel, setZoomLevel ] ); + + /** + * This hook tracks if the zoom state was changed manually by the user via clicking + * the zoom out button. + */ + useEffect( () => { + // If the zoom state changed (isZoomOut) and it does not match the requested zoom + // state (zoomOut), then it means the user manually changed the zoom state while + // this hook was mounted, and we should no longer control the zoom state. + if ( isZoomedOut !== enabled ) { + // Turn off all automatic zooming control. + controlZoomLevel.current = false; + } + + // Intentionally excluding `enabled` from the dependency array. We want to catch instances where + // the zoom out state changes due to user interaction and not due to the hook. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ isZoomedOut ] ); } diff --git a/test/e2e/specs/site-editor/site-editor-inserter.spec.js b/test/e2e/specs/site-editor/site-editor-inserter.spec.js index 04075cbedab30..c4e983ca35206 100644 --- a/test/e2e/specs/site-editor/site-editor-inserter.spec.js +++ b/test/e2e/specs/site-editor/site-editor-inserter.spec.js @@ -21,39 +21,40 @@ test.describe( 'Site Editor Inserter', () => { await editor.canvas.locator( 'body' ).click(); } ); + test.use( { + InserterUtils: async ( { editor, page }, use ) => { + await use( new InserterUtils( { editor, page } ) ); + }, + } ); + test( 'inserter toggle button should toggle global inserter', async ( { - page, + InserterUtils, } ) => { - await page.click( 'role=button[name="Block Inserter"i]' ); + const inserterButton = InserterUtils.getInserterButton(); + + await inserterButton.click(); + + const blockLibrary = InserterUtils.getBlockLibrary(); // Visibility check - await expect( - page.locator( 'role=searchbox[name="Search"i]' ) - ).toBeVisible(); - await page.click( 'role=button[name="Block Inserter"i]' ); + await expect( blockLibrary ).toBeVisible(); + await inserterButton.click(); //Hidden State check - await expect( - page.locator( 'role=searchbox[name="Search"i]' ) - ).toBeHidden(); + await expect( blockLibrary ).toBeHidden(); } ); // A test for https://github.com/WordPress/gutenberg/issues/43090. test( 'should close the inserter when clicking on the toggle button', async ( { - page, editor, + InserterUtils, } ) => { - const inserterButton = page.getByRole( 'button', { - name: 'Block Inserter', - exact: true, - } ); - const blockLibrary = page.getByRole( 'region', { - name: 'Block Library', - } ); + const inserterButton = InserterUtils.getInserterButton(); + const blockLibrary = InserterUtils.getBlockLibrary(); const beforeBlocks = await editor.getBlocks(); await inserterButton.click(); - await blockLibrary.getByRole( 'tab', { name: 'Blocks' } ).click(); + await InserterUtils.getBlockLibraryTab( 'Blocks' ).click(); await blockLibrary.getByRole( 'option', { name: 'Buttons' } ).click(); await expect @@ -64,4 +65,199 @@ test.describe( 'Site Editor Inserter', () => { await expect( blockLibrary ).toBeHidden(); } ); + + test( 'should open the inserter to patterns tab if using zoom out', async ( { + InserterUtils, + } ) => { + const zoomOutButton = InserterUtils.getZoomOutButton(); + const inserterButton = InserterUtils.getInserterButton(); + const blockLibrary = InserterUtils.getBlockLibrary(); + + await zoomOutButton.click(); + await expect( await InserterUtils.getZoomCanvas() ).toBeVisible(); + + await inserterButton.click(); + const patternsTab = InserterUtils.getBlockLibraryTab( 'Patterns' ); + await expect( patternsTab ).toHaveAttribute( + 'data-active-item', + 'true' + ); + await expect( await InserterUtils.getZoomCanvas() ).toBeVisible(); + + await inserterButton.click(); + + await expect( blockLibrary ).toBeHidden(); + + // We should still be in Zoom Out + await expect( await InserterUtils.getZoomCanvas() ).toBeVisible(); + } ); + + test( 'should enter zoom out from patterns tab and exit zoom out when closing the inserter', async ( { + InserterUtils, + } ) => { + const inserterButton = InserterUtils.getInserterButton(); + const blockLibrary = InserterUtils.getBlockLibrary(); + + await inserterButton.click(); + await expect( await InserterUtils.getZoomCanvas() ).toBeHidden(); + + const blocksTab = InserterUtils.getBlockLibraryTab( 'Blocks' ); + await expect( blocksTab ).toHaveAttribute( 'data-active-item', 'true' ); + + const patternsTab = InserterUtils.getBlockLibraryTab( 'Patterns' ); + await patternsTab.click(); + await expect( patternsTab ).toHaveAttribute( + 'data-active-item', + 'true' + ); + await expect( await InserterUtils.getZoomCanvas() ).toBeVisible(); + + await inserterButton.click(); + + await expect( blockLibrary ).toBeHidden(); + + await expect( await InserterUtils.getZoomCanvas() ).toBeHidden(); + } ); + + test( 'if starting from zoom out, blocks tab should not reset zoom level', async ( { + InserterUtils, + } ) => { + const zoomOutButton = InserterUtils.getZoomOutButton(); + const inserterButton = InserterUtils.getInserterButton(); + const blockLibrary = InserterUtils.getBlockLibrary(); + + // Manually enter zoom out + await zoomOutButton.click(); + await expect( await InserterUtils.getZoomCanvas() ).toBeVisible(); + + // Open inserter + await inserterButton.click(); + + // Patterns tab should be active + const patternsTab = InserterUtils.getBlockLibraryTab( 'Patterns' ); + await expect( patternsTab ).toHaveAttribute( + 'data-active-item', + 'true' + ); + // Canvas should be zoomed + await expect( await InserterUtils.getZoomCanvas() ).toBeVisible(); + + // // Select blocks tab + const blocksTab = InserterUtils.getBlockLibraryTab( 'Blocks' ); + await blocksTab.click(); + await expect( blocksTab ).toHaveAttribute( 'data-active-item', 'true' ); + + // Zoom out should still be engaged + await expect( await InserterUtils.getZoomCanvas() ).toBeVisible(); + + // Close the inserter + await inserterButton.click(); + await expect( blockLibrary ).toBeHidden(); + + // We should stayin zoom out since the inserter was opened with + // zoom out engaged + await expect( await InserterUtils.getZoomCanvas() ).toBeVisible(); + } ); + + // Test for https://github.com/WordPress/gutenberg/issues/66328 + test( 'should not return you to zoom out if manually disengaged', async ( { + InserterUtils, + } ) => { + const zoomOutButton = InserterUtils.getZoomOutButton(); + const inserterButton = InserterUtils.getInserterButton(); + const blockLibrary = InserterUtils.getBlockLibrary(); + + await zoomOutButton.click(); + await expect( await InserterUtils.getZoomCanvas() ).toBeVisible(); + + await inserterButton.click(); + const patternsTab = InserterUtils.getBlockLibraryTab( 'Patterns' ); + await expect( patternsTab ).toHaveAttribute( + 'data-active-item', + 'true' + ); + await expect( await InserterUtils.getZoomCanvas() ).toBeVisible(); + + await zoomOutButton.click(); + await expect( await InserterUtils.getZoomCanvas() ).toBeHidden(); + + // Close the inserter + await inserterButton.click(); + + await expect( blockLibrary ).toBeHidden(); + + // We should not return to zoom out since it was manually disengaged + await expect( await InserterUtils.getZoomCanvas() ).toBeHidden(); + } ); + + // Similar test to the above but starting from not zoomed in + test( 'should not toggle zoom state when closing the inserter if the user manually changed zoom state', async ( { + InserterUtils, + } ) => { + const zoomOutButton = InserterUtils.getZoomOutButton(); + const inserterButton = InserterUtils.getInserterButton(); + const blockLibrary = InserterUtils.getBlockLibrary(); + + await inserterButton.click(); + + // Go to patterns tab which should enter zoom out + const patternsTab = InserterUtils.getBlockLibraryTab( 'Patterns' ); + await patternsTab.click(); + await expect( patternsTab ).toHaveAttribute( + 'data-active-item', + 'true' + ); + await expect( await InserterUtils.getZoomCanvas() ).toBeVisible(); + + // Manually toggle zoom out off + await zoomOutButton.click(); + await expect( await InserterUtils.getZoomCanvas() ).toBeHidden(); + + // Manually toggle zoom out again to return to zoomed-in state set by the patterns tab. + await zoomOutButton.click(); + await expect( await InserterUtils.getZoomCanvas() ).toBeVisible(); + + // Close the inserter + await inserterButton.click(); + + await expect( blockLibrary ).toBeHidden(); + + // We should stay in zoomed out state since it was manually engaged + await expect( await InserterUtils.getZoomCanvas() ).toBeVisible(); + } ); } ); + +class InserterUtils { + constructor( { editor, page } ) { + this.editor = editor; + this.page = page; + } + + getInserterButton() { + return this.page.getByRole( 'button', { + name: 'Block Inserter', + exact: true, + } ); + } + + getBlockLibrary() { + return this.page.getByRole( 'region', { + name: 'Block Library', + } ); + } + + getBlockLibraryTab( name ) { + return this.page.getByRole( 'tab', { name } ); + } + + getZoomOutButton() { + return this.page.getByRole( 'button', { + name: 'Zoom Out', + exact: true, + } ); + } + + getZoomCanvas() { + return this.page.locator( '.is-zoomed-out' ); + } +}