diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index de482c5f059dc8..673562fde34f58 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -273,13 +273,16 @@ function Iframe( { src={ src } title={ __( 'Editor canvas' ) } onKeyDown={ ( event ) => { + if ( props.onKeyDown ) { + props.onKeyDown( event ); + } // If the event originates from inside the iframe, it means // it bubbled through the portal, but only with React // events. We need to to bubble native events as well, // though by doing so we also trigger another React event, // so we need to stop the propagation of this event to avoid // duplication. - if ( + else if ( event.currentTarget.ownerDocument !== event.target.ownerDocument ) { diff --git a/test/e2e/specs/site-editor/navigation.spec.js b/test/e2e/specs/site-editor/navigation.spec.js new file mode 100644 index 00000000000000..25a5b5dee59ffb --- /dev/null +++ b/test/e2e/specs/site-editor/navigation.spec.js @@ -0,0 +1,119 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.use( { + editorNavigationUtils: async ( { page, pageUtils }, use ) => { + await use( new EditorNavigationUtils( { page, pageUtils } ) ); + }, +} ); + +test.describe( 'Site editor navigation', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'emptytheme' ); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentyone' ); + } ); + + test( 'Can use keyboard to navigate the site editor', async ( { + admin, + editorNavigationUtils, + page, + pageUtils, + } ) => { + await admin.visitSiteEditor(); + + // Test: Can navigate to a sidebar item and into its subnavigation frame without losing focus + // Go to the Pages button + + await editorNavigationUtils.tabToLabel( 'Pages' ); + + await expect( + page.getByRole( 'button', { name: 'Pages' } ) + ).toBeFocused(); + await pageUtils.pressKeys( 'Enter' ); + // We should be in the Pages sidebar + await expect( + page.getByRole( 'button', { name: 'Back', exact: true } ) + ).toBeFocused(); + await pageUtils.pressKeys( 'Enter' ); + // Go back to the main navigation + await expect( + page.getByRole( 'button', { name: 'Pages' } ) + ).toBeFocused(); + + // Test: Can navigate into the iframe using the keyboard + await editorNavigationUtils.tabToLabel( 'Editor Canvas' ); + const editorCanvasButton = page.getByRole( 'button', { + name: 'Editor Canvas', + } ); + await expect( editorCanvasButton ).toBeFocused(); + // Enter into the site editor frame + await pageUtils.pressKeys( 'Enter' ); + // Focus should be on the iframe without the button role + await expect( + page.locator( 'iframe[name="editor-canvas"]' ) + ).toBeFocused(); + // The button role should have been removed from the iframe. + await expect( editorCanvasButton ).toBeHidden(); + + // Test to make sure a Tab keypress works as expected. + // As of this writing, we are in select mode and a tab + // keypress will reveal the header template select mode + // button. This test is not documenting that we _want_ + // that action, but checking that we are within the site + // editor and keypresses work as intened. + await pageUtils.pressKeys( 'Tab' ); + await expect( + page.getByRole( 'button', { + name: 'Template Part Block. Row 1. header', + } ) + ).toBeFocused(); + + // Test: We can go back to the main navigation from the editor frame + // Move to the document toolbar + await pageUtils.pressKeys( 'alt+F10' ); + // Go to the open navigation button + await pageUtils.pressKeys( 'shift+Tab' ); + + // Open the sidebar again + await expect( + page.getByRole( 'button', { + name: 'Open Navigation', + exact: true, + } ) + ).toBeFocused(); + await pageUtils.pressKeys( 'Enter' ); + + await expect( + page.getByLabel( 'Go to the Dashboard' ).first() + ).toBeFocused(); + // We should have our editor canvas button back + await expect( editorCanvasButton ).toBeVisible(); + } ); +} ); + +class EditorNavigationUtils { + constructor( { page, pageUtils } ) { + this.page = page; + this.pageUtils = pageUtils; + } + + async tabToLabel( label, times = 10 ) { + for ( let i = 0; i < times; i++ ) { + await this.pageUtils.pressKeys( 'Tab' ); + const activeLabel = await this.page.evaluate( () => { + return ( + document.activeElement.getAttribute( 'aria-label' ) || + document.activeElement.textContent + ); + } ); + if ( activeLabel === label ) { + return; + } + } + } +}