From 13a44f8e0be309d6e7b1569c8262729d43c3f98a Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Thu, 20 Aug 2020 12:34:41 +1000 Subject: [PATCH 1/7] Navigation: Add Post, Page, Category and Tag variations to Link Adds Post, Page, Category and Tag variations to the Navigation Link block. Each variation sets the type attribute which in turn causes LinkControl to filter its results using the /wp/v2/search API's type and subtype params. --- .../src/components/link-control/index.js | 3 + .../components/link-control/search-input.js | 2 + .../link-control/use-search-handler.js | 19 +++-- .../block-library/src/navigation-link/edit.js | 27 ++++++- .../src/navigation-link/index.js | 40 ++++++++++ packages/block-library/src/navigation/edit.js | 17 ++++- .../block-library/src/navigation/editor.scss | 5 ++ .../src/navigation/placeholder.js | 6 +- packages/edit-navigation/src/index.js | 74 +++++++++++-------- .../editor/src/components/provider/index.js | 74 +++++++++++-------- 10 files changed, 188 insertions(+), 79 deletions(-) diff --git a/packages/block-editor/src/components/link-control/index.js b/packages/block-editor/src/components/link-control/index.js index 9dba8d36721f1..aae2c36f509f6 100644 --- a/packages/block-editor/src/components/link-control/index.js +++ b/packages/block-editor/src/components/link-control/index.js @@ -88,6 +88,7 @@ import { ViewerFill } from './viewer-slot'; * @property {boolean=} showSuggestions Whether to present suggestions when typing the URL. * @property {boolean=} showInitialSuggestions Whether to present initial suggestions immediately. * @property {boolean=} withCreateSuggestion Whether to allow creation of link value from suggestion. + * @property {Object=} suggestionsQuery Query parameters to pass along to wp.blockEditor.__experimentalFetchLinkSuggestions. */ /** @@ -109,6 +110,7 @@ function LinkControl( { createSuggestion, withCreateSuggestion, inputValue: propInputValue = '', + suggestionsQuery = {}, } ) { if ( withCreateSuggestion === undefined && createSuggestion ) { withCreateSuggestion = true; @@ -209,6 +211,7 @@ function LinkControl( { showInitialSuggestions={ showInitialSuggestions } allowDirectEntry={ ! noDirectEntry } showSuggestions={ showSuggestions } + suggestionsQuery={ suggestionsQuery } >
diff --git a/packages/block-editor/src/components/link-control/search-input.js b/packages/block-editor/src/components/link-control/search-input.js index a4cde5b80cb8c..4e5dd10cad222 100644 --- a/packages/block-editor/src/components/link-control/search-input.js +++ b/packages/block-editor/src/components/link-control/search-input.js @@ -39,13 +39,16 @@ const LinkControlSearchInput = forwardRef( allowDirectEntry = true, showInitialSuggestions = false, suggestionsQuery = {}, + withURLSuggestion = true, + createSuggestionButtonText, }, ref ) => { const genericSearchHandler = useSearchHandler( suggestionsQuery, allowDirectEntry, - withCreateSuggestion + withCreateSuggestion, + withURLSuggestion ); const searchHandler = showSuggestions ? fetchSuggestions || genericSearchHandler @@ -77,6 +80,7 @@ const LinkControlSearchInput = forwardRef( instanceId, withCreateSuggestion, currentInputValue: value, + createSuggestionButtonText, handleSuggestionClick: ( suggestion ) => { if ( props.handleSuggestionClick ) { props.handleSuggestionClick( suggestion ); diff --git a/packages/block-editor/src/components/link-control/search-results.js b/packages/block-editor/src/components/link-control/search-results.js index db5db4920340e..2eac1f94ea0af 100644 --- a/packages/block-editor/src/components/link-control/search-results.js +++ b/packages/block-editor/src/components/link-control/search-results.js @@ -28,6 +28,7 @@ export default function LinkControlSearchResults( { selectedSuggestion, isLoading, isInitialSuggestions, + createSuggestionButtonText, } ) { const resultsListClasses = classnames( 'block-editor-link-control__search-results', @@ -87,6 +88,7 @@ export default function LinkControlSearchResults( { return ( handleSuggestionClick( suggestion ) } diff --git a/packages/block-editor/src/components/link-control/use-search-handler.js b/packages/block-editor/src/components/link-control/use-search-handler.js index e2d5f54a324df..ef5e25f0765fa 100644 --- a/packages/block-editor/src/components/link-control/use-search-handler.js +++ b/packages/block-editor/src/components/link-control/use-search-handler.js @@ -50,8 +50,11 @@ const handleEntitySearch = async ( suggestionsQuery, fetchSearchSuggestions, directEntryHandler, - withCreateSuggestion + withCreateSuggestion, + withURLSuggestion ) => { + const { isInitialSuggestions } = suggestionsQuery; + let results = await Promise.all( [ fetchSearchSuggestions( val, suggestionsQuery ), directEntryHandler( val ), @@ -62,13 +65,14 @@ const handleEntitySearch = async ( // If it's potentially a URL search then concat on a URL search suggestion // just for good measure. That way once the actual results run out we always // have a URL option to fallback on. - results = - couldBeURL && ! suggestionsQuery.isInitialSuggestions - ? results[ 0 ].concat( results[ 1 ] ) - : results[ 0 ]; + if ( couldBeURL && withURLSuggestion && ! isInitialSuggestions ) { + results = results[ 0 ].concat( results[ 1 ] ); + } else { + results = results[ 0 ]; + } // If displaying initial suggestions just return plain results. - if ( suggestionsQuery.isInitialSuggestions ) { + if ( isInitialSuggestions ) { return results; } @@ -101,7 +105,8 @@ const handleEntitySearch = async ( export default function useSearchHandler( suggestionsQuery, allowDirectEntry, - withCreateSuggestion + withCreateSuggestion, + withURLSuggestion ) { const { fetchSearchSuggestions } = useSelect( ( select ) => { const { getSettings } = select( 'core/block-editor' ); @@ -124,7 +129,8 @@ export default function useSearchHandler( { ...suggestionsQuery, isInitialSuggestions }, fetchSearchSuggestions, directEntryHandler, - withCreateSuggestion + withCreateSuggestion, + withURLSuggestion ); }, [ directEntryHandler, fetchSearchSuggestions, withCreateSuggestion ] diff --git a/packages/block-library/src/navigation-link/edit.js b/packages/block-library/src/navigation-link/edit.js index 2de72550f8123..706bfc43f79a0 100644 --- a/packages/block-library/src/navigation-link/edit.js +++ b/packages/block-library/src/navigation-link/edit.js @@ -25,7 +25,7 @@ import { ToolbarGroup, } from '@wordpress/components'; import { rawShortcut, displayShortcut } from '@wordpress/keycodes'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { BlockControls, InnerBlocks, @@ -35,7 +35,13 @@ import { __experimentalBlock as Block, } from '@wordpress/block-editor'; import { isURL, prependHTTP } from '@wordpress/url'; -import { Fragment, useState, useEffect, useRef } from '@wordpress/element'; +import { + Fragment, + useState, + useEffect, + useRef, + createInterpolateElement, +} from '@wordpress/element'; import { placeCaretAtHorizontalEdge } from '@wordpress/dom'; import { link as linkIcon } from '@wordpress/icons'; @@ -128,6 +134,7 @@ function NavigationLinkEdit( { rgbBackgroundColor, selectedBlockHasDescendants, userCanCreatePages = false, + userCanCreatePosts = false, insertBlocksAfter, mergeBlocks, onReplace, @@ -199,15 +206,24 @@ function NavigationLinkEdit( { selection.addRange( range ); } - async function handleCreatePage( pageTitle ) { - const page = await saveEntityRecord( 'postType', 'page', { + let userCanCreate = false; + if ( ! type || type === 'page' ) { + userCanCreate = userCanCreatePages; + } else if ( type === 'post' ) { + userCanCreate = userCanCreatePosts; + } + + async function handleCreate( pageTitle ) { + const postType = type || 'page'; + + const page = await saveEntityRecord( 'postType', postType, { title: pageTitle, status: 'publish', } ); return { id: page.id, - type, + postType, title: page.title.rendered, url: page.link, }; @@ -318,8 +334,28 @@ function NavigationLinkEdit( { className="wp-block-navigation-link__inline-link-input" value={ link } showInitialSuggestions={ true } - withCreateSuggestion={ userCanCreatePages } - createSuggestion={ handleCreatePage } + withCreateSuggestion={ userCanCreate } + createSuggestion={ handleCreate } + createSuggestionButtonText={ ( searchTerm ) => { + let format; + if ( type === 'post' ) { + /* translators: %s: search term. */ + format = __( + 'Create post: %s' + ); + } else { + /* translators: %s: search term. */ + format = __( + 'Create page: %s' + ); + } + return createInterpolateElement( + sprintf( format, searchTerm ), + { mark: } + ); + } } + noDirectEntry={ !! type } + noURLSuggestion={ !! type } suggestionsQuery={ getSuggestionsQuery( type ) } onChange={ ( { title: newTitle = '', @@ -445,11 +481,6 @@ export default compose( [ selectedBlockId, ] )?.length; - const userCanCreatePages = select( 'core' ).canUser( - 'create', - 'pages' - ); - return { isParentOfSelectedBlock, isImmediateParentOfSelectedBlock, @@ -458,7 +489,8 @@ export default compose( [ showSubmenuIcon, textColor: navigationBlockAttributes.textColor, backgroundColor: navigationBlockAttributes.backgroundColor, - userCanCreatePages, + userCanCreatePages: select( 'core' ).canUser( 'create', 'pages' ), + userCanCreatePosts: select( 'core' ).canUser( 'create', 'posts' ), rgbTextColor: getColorObjectByColorSlug( colors, navigationBlockAttributes.textColor, From 02d6eaf2f08aa6e2b356232b32b5b15a23ad57f7 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Wed, 26 Aug 2020 14:39:12 +1000 Subject: [PATCH 4/7] Navigation: Display 'post_tag' as 'tag' --- .../block-editor/src/components/link-control/search-item.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/link-control/search-item.js b/packages/block-editor/src/components/link-control/search-item.js index a7ef58e36d491..940121c0e0fe4 100644 --- a/packages/block-editor/src/components/link-control/search-item.js +++ b/packages/block-editor/src/components/link-control/search-item.js @@ -56,7 +56,8 @@ export const LinkControlSearchItem = ( { { suggestion.type && ( - { suggestion.type } + { /* Rename 'post_tag' to 'tag'. Ideally, the API would return the localised CPT or taxonomy label. */ } + { suggestion.type === 'post_tag' ? 'tag' : suggestion.type } ) } From 467ed277375d6fcf8807890ff27509e501583dd4 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Wed, 26 Aug 2020 15:39:28 +1000 Subject: [PATCH 5/7] Fix LinkControl unit tests and Navigation E2E tests --- .../link-control/test/fixtures/index.js | 6 ++-- .../src/components/link-control/test/index.js | 10 +++--- .../__snapshots__/navigation.test.js.snap | 6 +--- .../specs/experiments/navigation.test.js | 31 +++++++++++-------- 4 files changed, 27 insertions(+), 26 deletions(-) diff --git a/packages/block-editor/src/components/link-control/test/fixtures/index.js b/packages/block-editor/src/components/link-control/test/fixtures/index.js index b470155977a30..173d7963a8e85 100644 --- a/packages/block-editor/src/components/link-control/test/fixtures/index.js +++ b/packages/block-editor/src/components/link-control/test/fixtures/index.js @@ -38,10 +38,10 @@ export const fauxEntitySuggestions = [ /* eslint-disable no-unused-vars */ export const fetchFauxEntitySuggestions = ( val = '', - { perPage = null } = {} + { isInitialSuggestions } = {} ) => { - const suggestions = perPage - ? take( fauxEntitySuggestions, perPage ) + const suggestions = isInitialSuggestions + ? take( fauxEntitySuggestions, 3 ) : fauxEntitySuggestions; return Promise.resolve( suggestions ); }; diff --git a/packages/block-editor/src/components/link-control/test/index.js b/packages/block-editor/src/components/link-control/test/index.js index 3732b658d72a1..54c87968a0026 100644 --- a/packages/block-editor/src/components/link-control/test/index.js +++ b/packages/block-editor/src/components/link-control/test/index.js @@ -725,7 +725,7 @@ describe( 'Creating Entities (eg: Posts, Pages)', () => { const createButton = first( Array.from( searchResultElements ).filter( ( result ) => - result.innerHTML.includes( 'New page' ) + result.innerHTML.includes( 'Create:' ) ) ); @@ -822,7 +822,7 @@ describe( 'Creating Entities (eg: Posts, Pages)', () => { const createButton = first( Array.from( searchResultElements ).filter( ( result ) => - result.innerHTML.includes( 'New page' ) + result.innerHTML.includes( 'Create:' ) ) ); @@ -895,7 +895,7 @@ describe( 'Creating Entities (eg: Posts, Pages)', () => { const form = container.querySelector( 'form' ); const createButton = first( Array.from( searchResultElements ).filter( ( result ) => - result.innerHTML.includes( 'New page' ) + result.innerHTML.includes( 'Create:' ) ) ); @@ -949,7 +949,7 @@ describe( 'Creating Entities (eg: Posts, Pages)', () => { ); const createButton = first( Array.from( searchResultElements ).filter( ( result ) => - result.innerHTML.includes( 'New page' ) + result.innerHTML.includes( 'Create:' ) ) ); @@ -1074,7 +1074,7 @@ describe( 'Creating Entities (eg: Posts, Pages)', () => { ); let createButton = first( Array.from( searchResultElements ).filter( ( result ) => - result.innerHTML.includes( 'New page' ) + result.innerHTML.includes( 'Create:' ) ) ); diff --git a/packages/e2e-tests/specs/experiments/__snapshots__/navigation.test.js.snap b/packages/e2e-tests/specs/experiments/__snapshots__/navigation.test.js.snap index c575b3297d2a2..eb7a6c163f2f3 100644 --- a/packages/e2e-tests/specs/experiments/__snapshots__/navigation.test.js.snap +++ b/packages/e2e-tests/specs/experiments/__snapshots__/navigation.test.js.snap @@ -36,11 +36,7 @@ exports[`Navigation Creating from existing Menus allows a navigation block to be " `; -exports[`Navigation Creating from existing Menus creates an empty navigation block when the selected existing menu is also empty 1`] = ` -" - -" -`; +exports[`Navigation Creating from existing Menus creates an empty navigation block when the selected existing menu is also empty 1`] = `""`; exports[`Navigation Creating from existing Menus does not display option to create from existing menus if there are no menus 1`] = `""`; diff --git a/packages/e2e-tests/specs/experiments/navigation.test.js b/packages/e2e-tests/specs/experiments/navigation.test.js index d49a723e1de74..7b2a81824de7a 100644 --- a/packages/e2e-tests/specs/experiments/navigation.test.js +++ b/packages/e2e-tests/specs/experiments/navigation.test.js @@ -249,6 +249,17 @@ async function createEmptyNavBlock() { await clickCreateButton(); } +async function addLinkBlock() { + // Using 'click' here checks for regressions of https://github.com/WordPress/gutenberg/issues/18329, + // an issue where the block appender requires two clicks. + await page.click( '.wp-block-navigation .block-list-appender' ); + + const [ linkButton ] = await page.$x( + "//*[contains(@class, 'block-editor-inserter__quick-inserter')]//*[text()='Link']" + ); + await linkButton.click(); +} + beforeEach( async () => { await createNewPost(); } ); @@ -364,8 +375,7 @@ describe( 'Navigation', () => { ); // Assert an empty Nav Block is created. - // We expect 1 here because a "placeholder" Nav Item Block is automatically inserted - expect( navBlockItemsLength ).toEqual( 1 ); + expect( navBlockItemsLength ).toEqual( 0 ); // Snapshot should contain the mocked menu items. expect( await getEditedPostContent() ).toMatchSnapshot(); @@ -410,7 +420,9 @@ describe( 'Navigation', () => { await createEmptyNavBlock(); - // Add a link to the default Link block. + await addLinkBlock(); + + // Add a link to the Link block. await updateActiveNavigationLink( { url: 'https://wordpress.org', label: 'WP', @@ -419,16 +431,7 @@ describe( 'Navigation', () => { await showBlockToolbar(); - // Add another block. - // Using 'click' here checks for regressions of https://github.com/WordPress/gutenberg/issues/18329, - // an issue where the block appender requires two clicks. - await page.click( '.wp-block-navigation .block-list-appender' ); - - // Select a Link block. - const [ linkButton ] = await page.$x( - "//*[contains(@class, 'block-editor-inserter__quick-inserter')]//*[text()='Link']" - ); - await linkButton.click(); + await addLinkBlock(); // After adding a new block, search input should be shown immediately. // Verify that Escape would close the popover. @@ -496,6 +499,8 @@ describe( 'Navigation', () => { // Create an empty nav block. await createEmptyNavBlock(); + await addLinkBlock(); + // Wait for URL input to be focused await page.waitForSelector( 'input.block-editor-url-input__input:focus' From 9bb7317a6d0fdfa94b5520f4435a5d1fcf21f291 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Wed, 26 Aug 2020 15:57:39 +1000 Subject: [PATCH 6/7] LinkControl: Add tests for noURLSuggestion and createSuggestionButtonText --- .../src/components/link-control/test/index.js | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/packages/block-editor/src/components/link-control/test/index.js b/packages/block-editor/src/components/link-control/test/index.js index 54c87968a0026..0dfaaef70c690 100644 --- a/packages/block-editor/src/components/link-control/test/index.js +++ b/packages/block-editor/src/components/link-control/test/index.js @@ -394,6 +394,33 @@ describe( 'Searching for a link', () => { ); } ); + + it( 'should not display a URL suggestion as a default fallback when noURLSuggestion is passed.', async () => { + act( () => { + render( , container ); + } ); + + // Search Input UI + const searchInput = getURLInput(); + + // Simulate searching for a term + act( () => { + Simulate.change( searchInput, { + target: { value: 'couldbeurlorentitysearchterm' }, + } ); + } ); + + // fetchFauxEntitySuggestions resolves on next "tick" of event loop + await eventLoopTick(); + // TODO: select these by aria relationship to autocomplete rather than arbitrary selector. + + const searchResultElements = getSearchResults(); + + // We should see a search result for each of the expect search suggestions and nothing else + expect( searchResultElements ).toHaveLength( + fauxEntitySuggestions.length + ); + } ); } ); describe( 'Manual link entry', () => { @@ -925,6 +952,50 @@ describe( 'Creating Entities (eg: Posts, Pages)', () => { ); } ); + it( 'should allow customisation of button text', async () => { + const entityNameText = 'A new page to be created'; + + const LinkControlConsumer = () => { + return ( + {} } + createSuggestionButtonText="Custom suggestion text" + /> + ); + }; + + act( () => { + render( , container ); + } ); + + // Search Input UI + const searchInput = container.querySelector( + 'input[aria-label="URL"]' + ); + + // Simulate searching for a term + act( () => { + Simulate.change( searchInput, { + target: { value: entityNameText }, + } ); + } ); + + await eventLoopTick(); + + // TODO: select these by aria relationship to autocomplete rather than arbitrary selector. + const searchResultElements = container.querySelectorAll( + '[role="listbox"] [role="option"]' + ); + + const createButton = first( + Array.from( searchResultElements ).filter( ( result ) => + result.innerHTML.includes( 'Custom suggestion text' ) + ) + ); + + expect( createButton ).not.toBeNull(); + } ); + describe( 'Do not show create option', () => { it.each( [ [ undefined ], [ null ], [ false ] ] )( 'should not show not show an option to create an entity when "createSuggestion" handler is %s', From 29d03bb650a0f968ee5e9fcd903808cba72dc23b Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Wed, 26 Aug 2020 17:12:36 +1000 Subject: [PATCH 7/7] LinkControl: Improve JSDoc comment --- packages/block-editor/src/components/link-control/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/block-editor/src/components/link-control/index.js b/packages/block-editor/src/components/link-control/index.js index c45410cb7a9bd..9698ceeb3d747 100644 --- a/packages/block-editor/src/components/link-control/index.js +++ b/packages/block-editor/src/components/link-control/index.js @@ -84,13 +84,13 @@ import { ViewerFill } from './viewer-slot'; * @property {WPLinkControlValue=} value Current link value. * @property {WPLinkControlOnChangeProp=} onChange Value change handler, called with the updated value if * the user selects a new link or updates settings. - * @property {boolean=} noDirectEntry Whether to disable direct entries or not. + * @property {boolean=} noDirectEntry Whether to allow turning a URL-like search query directly into a link. * @property {boolean=} showSuggestions Whether to present suggestions when typing the URL. * @property {boolean=} showInitialSuggestions Whether to present initial suggestions immediately. * @property {boolean=} withCreateSuggestion Whether to allow creation of link value from suggestion. * @property {Object=} suggestionsQuery Query parameters to pass along to wp.blockEditor.__experimentalFetchLinkSuggestions. - * @property {boolean=} noURLSuggestion Whether to disable suggesting the search query as a URL. - * @property {string|Function|undefined} createSuggestionButtonText Text to use in the button that creates a suggestion. + * @property {boolean=} noURLSuggestion Whether to add a fallback suggestion which treats the search query as a URL. + * @property {string|Function|undefined} createSuggestionButtonText The text to use in the button that calls createSuggestion. */ /**