diff --git a/packages/block-editor/src/hooks/use-zoom-out.js b/packages/block-editor/src/hooks/use-zoom-out.js index bcf5d9ff882f7b..fd409c8465790b 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,23 +21,67 @@ export function useZoomOut( zoomOut = true ) { ); const { isZoomOut } = unlock( useSelect( blockEditorStore ) ); + const toggleZoomOnUnmount = useRef( false ); + const userZoomState = useRef( null ); + const isZoomedOut = isZoomOut(); + useEffect( () => { - const isZoomOutOnMount = isZoomOut(); + // Store the user's manually set zoom state on mount. + userZoomState.current = isZoomOut(); return () => { - if ( isZoomOutOnMount ) { - setZoomLevel( 'auto-scaled' ); - } else { + if ( ! toggleZoomOnUnmount.current ) { + return; + } + + if ( isZoomOut() ) { resetZoomLevel(); + } else { + setZoomLevel( 'auto-scaled' ); } }; }, [] ); + /** + * This hook should only run when the requested zoomOut changes. We don't want to + * update it when isZoomedOut changes. + */ useEffect( () => { - if ( zoomOut ) { - setZoomLevel( 'auto-scaled' ); - } else { - resetZoomLevel(); + // Requested zoom and current zoom states are different, so toggle the state. + if ( zoomOut !== isZoomedOut ) { + // If the requested zoomOut matches the user's manually set zoom state, + // do not toggle the zoom level on unmount. + if ( userZoomState.current === zoomOut ) { + toggleZoomOnUnmount.current = false; + } else { + toggleZoomOnUnmount.current = true; + } + + if ( isZoomedOut ) { + resetZoomLevel(); + } else { + setZoomLevel( 'auto-scaled' ); + } } + // Intentionally excluding isZoomedOut so this hook will not recursively udpate. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ zoomOut, setZoomLevel, resetZoomLevel ] ); + + /** + * 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 and we should + // not toggle the zoom level on unmount. + if ( isZoomedOut !== zoomOut ) { + toggleZoomOnUnmount.current = false; + userZoomState.current = zoomOut; + } + + // Intentionally excluding zoomOut 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 04075cbedab308..459d8fadbe30b8 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,241 @@ 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( 'should return you to zoom out if starting from zoom out', 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 disengage + await expect( await InserterUtils.getZoomCanvas() ).toBeHidden(); + + // Close the inserter + await inserterButton.click(); + await expect( blockLibrary ).toBeHidden(); + + // We should return to zoom out since the inserter was opened with + // zoom out engaged, and it was automatically disengaged when selecting + // the blocks tab + 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(); + } ); + + // Covers bug found in https://github.com/WordPress/gutenberg/pull/66381#issuecomment-2441540851 + test( 'should return to initial zoom out state after switching between multiple tabs', 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' ); + const blocksTab = InserterUtils.getBlockLibraryTab( 'Blocks' ); + const mediaTab = InserterUtils.getBlockLibraryTab( 'Media' ); + + // Should start with pattern tab selected in zoom out state + await expect( patternsTab ).toHaveAttribute( + 'data-active-item', + 'true' + ); + await expect( await InserterUtils.getZoomCanvas() ).toBeVisible(); + + // Go to blocks tab which should exit zoom out + await blocksTab.click(); + await expect( blocksTab ).toHaveAttribute( 'data-active-item', 'true' ); + await expect( await InserterUtils.getZoomCanvas() ).toBeHidden(); + + // Go to media tab which should enter zoom out again since that's the starting state + await mediaTab.click(); + await expect( mediaTab ).toHaveAttribute( 'data-active-item', 'true' ); + await expect( await InserterUtils.getZoomCanvas() ).toBeVisible(); + + // Close the inserter + await inserterButton.click(); + await expect( blockLibrary ).toBeHidden(); + + // We should re-enter zoomed out state since it was the starting point + 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' ); + } +}