diff --git a/packages/block-library/src/private-apis.js b/packages/block-library/src/private-apis.js index 449a78c8f52674..0982115ed0442f 100644 --- a/packages/block-library/src/private-apis.js +++ b/packages/block-library/src/private-apis.js @@ -3,6 +3,7 @@ */ import { default as BlockKeyboardShortcuts } from './block-keyboard-shortcuts'; import { NAVIGATION_OVERLAY_TEMPLATE_PART_AREA } from './navigation/constants'; +import { NavigationLinkUI } from './navigation/edit/navigation-link-ui'; import { lock } from './lock-unlock'; /** @@ -12,4 +13,5 @@ export const privateApis = {}; lock( privateApis, { BlockKeyboardShortcuts, NAVIGATION_OVERLAY_TEMPLATE_PART_AREA, + NavigationLinkUI, } ); diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/navigation-menu-editor.js b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/navigation-menu-editor.js index 3e19c7f5a29fcb..2e76e7eebbc64d 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/navigation-menu-editor.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/navigation-menu-editor.js @@ -5,6 +5,7 @@ import { useMemo } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; import { BlockEditorProvider } from '@wordpress/block-editor'; import { createBlock } from '@wordpress/blocks'; +import { __experimentalFetchLinkSuggestions as fetchLinkSuggestions } from '@wordpress/core-data'; /** * Internal dependencies @@ -24,6 +25,14 @@ export default function NavigationMenuEditor( { navigationMenuId } ) { }; }, [] ); + const settings = useMemo( () => { + return { + ...storedSettings, + __experimentalFetchLinkSuggestions: ( search, searchOptions ) => + fetchLinkSuggestions( search, searchOptions, storedSettings ), + }; + }, [ storedSettings ] ); + const blocks = useMemo( () => { if ( ! navigationMenuId ) { return []; @@ -38,7 +47,7 @@ export default function NavigationMenuEditor( { navigationMenuId } ) { return ( ) } diff --git a/test/e2e/specs/site-editor/navigation-sidebar-list-view.spec.js b/test/e2e/specs/site-editor/navigation-sidebar-list-view.spec.js new file mode 100644 index 00000000000000..bfa1e7b5d7c10f --- /dev/null +++ b/test/e2e/specs/site-editor/navigation-sidebar-list-view.spec.js @@ -0,0 +1,178 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Navigation sidebar - list view editing', () => { + const navMenuFixture = { + title: 'Test Navigation Menu', + content: + '', + }; + + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'emptytheme' ); + await requestUtils.createPage( { + title: 'Test Page 1', + status: 'publish', + } ); + await requestUtils.createPage( { + title: 'Test Page 2', + status: 'publish', + } ); + } ); + + test.afterEach( async ( { requestUtils } ) => { + await requestUtils.deleteAllMenus(); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await Promise.all( [ + requestUtils.deleteAllPages(), + requestUtils.activateTheme( 'twentytwentyone' ), + ] ); + } ); + + test.use( { + linkControl: async ( { page }, use ) => { + await use( new LinkControl( { page } ) ); + }, + } ); + + test( 'can use appender in site editor sidebar list view', async ( { + admin, + page, + requestUtils, + linkControl, + } ) => { + const createdMenu = + await requestUtils.createNavigationMenu( navMenuFixture ); + + // Visit the site editor in sidebar-browse mode (without canvas: 'edit') + // so that the sidebar shows the navigation menu's list view. + await admin.visitSiteEditor( { + postId: createdMenu?.id, + postType: 'wp_navigation', + } ); + + const listView = page.getByRole( 'treegrid', { + name: 'Block navigation structure', + } ); + + await expect( listView ).toBeVisible(); + + // Verify the existing item is shown in the sidebar list view. + await expect( + listView.getByRole( 'link', { name: 'Existing Item' } ) + ).toBeVisible(); + + // The appender button should be present to allow adding new items. + const appender = listView.getByRole( 'button', { name: 'Add page' } ); + await expect( appender ).toBeVisible(); + + const linkControlSearch = linkControl.getLinkControlSearch(); + + await test.step( 'can add new menu items', async () => { + await appender.click(); + + // The LinkUI popover should open and immediately focus the search input. + await expect( linkControlSearch ).toBeFocused(); + + // Search for and select the page. + await linkControl.useLinkControlSearch( 'Test Page 2' ); + + // The new item should be appended after the existing item. + await expect( + listView.getByRole( 'link', { name: 'Test Page 2' } ) + ).toBeVisible(); + } ); + + await test.step( 'can open and close the Link UI without losing focus', async () => { + await page.keyboard.press( 'ArrowDown' ); + await expect( appender ).toBeFocused(); + await page.keyboard.press( 'Enter' ); + await expect( linkControlSearch ).toBeFocused(); + await page.keyboard.press( 'Escape' ); + await expect( linkControlSearch ).toBeHidden(); + } ); + + await test.step( 'can create a new page', async () => { + await appender.click(); + + // The search input should be focused immediately. + await expect( linkControl.getLinkControlSearch() ).toBeFocused(); + + // Type a new page title that doesn't exist yet. + await page.keyboard.type( 'Brand New Page', { delay: 50 } ); + + // Tab twice to reach the "Create page" button. + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Tab' ); + + const createPageButton = page.getByRole( 'button', { + name: 'Create page', + } ); + await expect( createPageButton ).toBeVisible(); + await expect( createPageButton ).toBeFocused(); + + // Open the page creation form. + await page.keyboard.press( 'Enter' ); + + // The title field should be pre-populated with the typed text. + const titleField = page.getByRole( 'textbox', { name: 'Title' } ); + await expect( titleField ).toHaveValue( 'Brand New Page' ); + + // The Back button should be focused after entering the creation form. + const backButton = page.locator( '.link-ui-page-creator__back' ); + await expect( backButton ).toBeFocused(); + + // Tab to the title field. + await page.keyboard.press( 'Tab' ); + await expect( titleField ).toBeFocused(); + + // Tab to the Publish checkbox (on by default). + await page.keyboard.press( 'Tab' ); + const publishCheckbox = page.getByRole( 'checkbox', { + name: 'Publish', + } ); + await expect( publishCheckbox ).toBeFocused(); + await expect( publishCheckbox ).toBeChecked(); + + // Tab twice more to reach the Create page button. + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Tab' ); + await expect( createPageButton ).toBeFocused(); + await page.keyboard.press( 'Enter' ); + + // The newly created page should appear as a new item in the list view. + await expect( + listView.getByRole( 'link', { name: 'Brand New Page' } ) + ).toBeVisible(); + } ); + } ); +} ); + +class LinkControl { + constructor( { page } ) { + this.page = page; + } + + getLinkControlSearch() { + return this.page.getByRole( 'combobox', { + name: 'Search or type URL', + } ); + } + + async useLinkControlSearch( searchTerm ) { + await expect( this.getLinkControlSearch() ).toBeFocused(); + + await this.page.keyboard.type( searchTerm, { delay: 50 } ); + + await expect( + this.page.getByRole( 'listbox', { name: 'Search results' } ) + ).toBeVisible(); + + await this.page.keyboard.press( 'ArrowDown' ); + await this.page.keyboard.press( 'Enter' ); + } +}