Skip to content

Commit

Permalink
Navigation: Add Post, Page, Category and Tag variations to Link (#24670)
Browse files Browse the repository at this point in the history
* 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.

* Navigation: Add icons to each of the Link variations

* Navigation: Show 'Create post' or 'Create page' when appropriate

- Display 'Create post' when inserting a Post Link.
- Display 'Create page' when inserting a Link or a Page Link.
- Only allow direct URL input when inserting a Link.

* Navigation: Display 'post_tag' as 'tag'

* Fix LinkControl unit tests and Navigation E2E tests

* LinkControl: Add tests for noURLSuggestion and createSuggestionButtonText

* LinkControl: Improve JSDoc comment
  • Loading branch information
noisysocks authored Aug 26, 2020
1 parent aea4eba commit 0d90505
Show file tree
Hide file tree
Showing 17 changed files with 400 additions and 147 deletions.
35 changes: 23 additions & 12 deletions packages/block-editor/src/components/link-control/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,18 +76,21 @@ import { ViewerFill } from './viewer-slot';
/**
* @typedef WPLinkControlProps
*
* @property {(WPLinkControlSetting[])=} settings An array of settings objects. Each object will used to
* render a `ToggleControl` for that setting.
* @property {boolean=} forceIsEditingLink If passed as either `true` or `false`, controls the
* internal editing state of the component to respective
* show or not show the URL input field.
* @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=} 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 {(WPLinkControlSetting[])=} settings An array of settings objects. Each object will used to
* render a `ToggleControl` for that setting.
* @property {boolean=} forceIsEditingLink If passed as either `true` or `false`, controls the
* internal editing state of the component to respective
* show or not show the URL input field.
* @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 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 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.
*/

/**
Expand All @@ -109,6 +112,9 @@ function LinkControl( {
createSuggestion,
withCreateSuggestion,
inputValue: propInputValue = '',
suggestionsQuery = {},
noURLSuggestion = false,
createSuggestionButtonText,
} ) {
if ( withCreateSuggestion === undefined && createSuggestion ) {
withCreateSuggestion = true;
Expand Down Expand Up @@ -209,6 +215,11 @@ function LinkControl( {
showInitialSuggestions={ showInitialSuggestions }
allowDirectEntry={ ! noDirectEntry }
showSuggestions={ showSuggestions }
suggestionsQuery={ suggestionsQuery }
withURLSuggestion={ ! noURLSuggestion }
createSuggestionButtonText={
createSuggestionButtonText
}
>
<div className="block-editor-link-control__search-actions">
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* External dependencies
*/
import classnames from 'classnames';
import { isFunction } from 'lodash';

/**
* WordPress dependencies
Expand All @@ -15,11 +16,26 @@ export const LinkControlSearchCreate = ( {
onClick,
itemProps,
isSelected,
buttonText,
} ) => {
if ( ! searchTerm ) {
return null;
}

let text;
if ( buttonText ) {
text = isFunction( buttonText ) ? buttonText( searchTerm ) : buttonText;
} else {
text = createInterpolateElement(
sprintf(
/* translators: %s: search term. */
__( 'Create: <mark>%s</mark>' ),
searchTerm
),
{ mark: <mark /> }
);
}

return (
<Button
{ ...itemProps }
Expand All @@ -38,14 +54,7 @@ export const LinkControlSearchCreate = ( {

<span className="block-editor-link-control__search-item-header">
<span className="block-editor-link-control__search-item-title">
{ createInterpolateElement(
sprintf(
/* translators: %s: search term. */
__( 'New page: <mark>%s</mark>' ),
searchTerm
),
{ mark: <mark /> }
) }
{ text }
</span>
</span>
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,17 @@ const LinkControlSearchInput = forwardRef(
fetchSuggestions = null,
allowDirectEntry = true,
showInitialSuggestions = false,
suggestionsQuery = {},
withURLSuggestion = true,
createSuggestionButtonText,
},
ref
) => {
const genericSearchHandler = useSearchHandler(
suggestionsQuery,
allowDirectEntry,
withCreateSuggestion
withCreateSuggestion,
withURLSuggestion
);
const searchHandler = showSuggestions
? fetchSuggestions || genericSearchHandler
Expand Down Expand Up @@ -75,6 +80,7 @@ const LinkControlSearchInput = forwardRef(
instanceId,
withCreateSuggestion,
currentInputValue: value,
createSuggestionButtonText,
handleSuggestionClick: ( suggestion ) => {
if ( props.handleSuggestionClick ) {
props.handleSuggestionClick( suggestion );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ export const LinkControlSearchItem = ( {
</span>
{ suggestion.type && (
<span className="block-editor-link-control__search-item-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 }
</span>
) }
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default function LinkControlSearchResults( {
selectedSuggestion,
isLoading,
isInitialSuggestions,
createSuggestionButtonText,
} ) {
const resultsListClasses = classnames(
'block-editor-link-control__search-results',
Expand Down Expand Up @@ -87,6 +88,7 @@ export default function LinkControlSearchResults( {
return (
<LinkControlSearchCreate
searchTerm={ currentInputValue }
buttonText={ createSuggestionButtonText }
onClick={ () =>
handleSuggestionClick( suggestion )
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
};
Expand Down
81 changes: 76 additions & 5 deletions packages/block-editor/src/components/link-control/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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( <LinkControl noURLSuggestion />, 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', () => {
Expand Down Expand Up @@ -725,7 +752,7 @@ describe( 'Creating Entities (eg: Posts, Pages)', () => {

const createButton = first(
Array.from( searchResultElements ).filter( ( result ) =>
result.innerHTML.includes( 'New page' )
result.innerHTML.includes( 'Create:' )
)
);

Expand Down Expand Up @@ -822,7 +849,7 @@ describe( 'Creating Entities (eg: Posts, Pages)', () => {

const createButton = first(
Array.from( searchResultElements ).filter( ( result ) =>
result.innerHTML.includes( 'New page' )
result.innerHTML.includes( 'Create:' )
)
);

Expand Down Expand Up @@ -895,7 +922,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:' )
)
);

Expand Down Expand Up @@ -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 (
<LinkControl
createSuggestion={ () => {} }
createSuggestionButtonText="Custom suggestion text"
/>
);
};

act( () => {
render( <LinkControlConsumer />, 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',
Expand All @@ -949,7 +1020,7 @@ describe( 'Creating Entities (eg: Posts, Pages)', () => {
);
const createButton = first(
Array.from( searchResultElements ).filter( ( result ) =>
result.innerHTML.includes( 'New page' )
result.innerHTML.includes( 'Create:' )
)
);

Expand Down Expand Up @@ -1074,7 +1145,7 @@ describe( 'Creating Entities (eg: Posts, Pages)', () => {
);
let createButton = first(
Array.from( searchResultElements ).filter( ( result ) =>
result.innerHTML.includes( 'New page' )
result.innerHTML.includes( 'Create:' )
)
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,18 @@ export const handleDirectEntry = ( val ) => {
] );
};

export const handleEntitySearch = async (
const handleEntitySearch = async (
val,
args,
suggestionsQuery,
fetchSearchSuggestions,
directEntryHandler,
withCreateSuggestion
withCreateSuggestion,
withURLSuggestion
) => {
const { isInitialSuggestions } = suggestionsQuery;

let results = await Promise.all( [
fetchSearchSuggestions( val, {
...( args.isInitialSuggestions ? { perPage: 3 } : {} ),
} ),
fetchSearchSuggestions( val, suggestionsQuery ),
directEntryHandler( val ),
] );

Expand All @@ -64,13 +65,14 @@ export 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 && ! args.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 ( args.isInitialSuggestions ) {
if ( isInitialSuggestions ) {
return results;
}

Expand Down Expand Up @@ -101,8 +103,10 @@ export const handleEntitySearch = async (
};

export default function useSearchHandler(
suggestionsQuery,
allowDirectEntry,
withCreateSuggestion
withCreateSuggestion,
withURLSuggestion
) {
const { fetchSearchSuggestions } = useSelect( ( select ) => {
const { getSettings } = select( 'core/block-editor' );
Expand All @@ -117,15 +121,16 @@ export default function useSearchHandler(
: handleNoop;

return useCallback(
( val, args ) => {
( val, { isInitialSuggestions } ) => {
return isURLLike( val )
? directEntryHandler( val, args )
? directEntryHandler( val, { isInitialSuggestions } )
: handleEntitySearch(
val,
args,
{ ...suggestionsQuery, isInitialSuggestions },
fetchSearchSuggestions,
directEntryHandler,
withCreateSuggestion
withCreateSuggestion,
withURLSuggestion
);
},
[ directEntryHandler, fetchSearchSuggestions, withCreateSuggestion ]
Expand Down
Loading

0 comments on commit 0d90505

Please sign in to comment.