Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Block Editor: Generate consistent suggestion object by URL #19827

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* WordPress dependencies
*/
import { prependHTTP, getProtocol, isEmail } from '@wordpress/url';

/**
* Given an input value assumed to be interpreted as a URL, returns a suggestion
* value representing that URL. The suggestion will always assign a `type` of
* `'url'`, an ID and URL corresponding to the given input value, and an empty
* title. The subtype will be inferred based on an interpretation of the URL,
* one of: `'fragment'`, `'query'`, `'path'`, or the scheme of the URL.
*
* @param {string} url Input value.
*
* @return {WPLinkControlSuggestion} URL suggestion.
*/
function getSuggestionByURL( url ) {
if ( isEmail( url ) ) {
url = 'mailto:' + url;
}

url = prependHTTP( url );

let subtype;
if ( url[ 0 ] === '#' ) {
subtype = 'fragment';
} else if ( url[ 0 ] === '?' ) {
subtype = 'query';
} else if ( url[ 0 ] === '/' || url[ 0 ] === '.' ) {
subtype = 'path';
} else {
const protocol = getProtocol( url );
subtype = protocol.replace( /:$/, '' );
}

return {
id: url,
title: '',
type: 'url',
subtype,
url,
};
}

export default getSuggestionByURL;
77 changes: 11 additions & 66 deletions packages/block-editor/src/components/link-control/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,15 @@
* External dependencies
*/
import classnames from 'classnames';
import { noop, startsWith } from 'lodash';
import { noop } from 'lodash';

/**
* WordPress dependencies
*/
import { Button, ExternalLink, VisuallyHidden } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import { useCallback, useState, Fragment } from '@wordpress/element';
import {
safeDecodeURI,
filterURLForDisplay,
isURL,
prependHTTP,
getProtocol,
} from '@wordpress/url';
import { useState, Fragment } from '@wordpress/element';
import { safeDecodeURI, filterURLForDisplay } from '@wordpress/url';
import { useInstanceId } from '@wordpress/compose';
import { useSelect } from '@wordpress/data';

Expand All @@ -26,6 +20,7 @@ import { useSelect } from '@wordpress/data';
import LinkControlSettingsDrawer from './settings-drawer';
import LinkControlSearchItem from './search-item';
import LinkControlSearchInput from './search-input';
import getSuggestionByURL from './get-suggestion-by-url';

function LinkControl( {
value,
Expand Down Expand Up @@ -57,68 +52,19 @@ function LinkControl( {
setInputValue( '' );
};

const handleDirectEntry = ( val ) => {
let type = 'URL';

const protocol = getProtocol( val ) || '';

if ( protocol.includes( 'mailto' ) ) {
type = 'mailto';
}

if ( protocol.includes( 'tel' ) ) {
type = 'tel';
}

if ( startsWith( val, '#' ) ) {
type = 'internal';
}

return Promise.resolve(
[ {
id: '-1',
title: val,
url: type === 'URL' ? prependHTTP( val ) : val,
type,
} ]
);
};

const handleEntitySearch = async ( val, args ) => {
const results = await Promise.all( [
fetchSearchSuggestions( val, {
...( args.isInitialSuggestions ? { perPage: 3 } : {} ),
} ),
handleDirectEntry( val ),
] );

const couldBeURL = ! val.includes( ' ' );

// 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.
return couldBeURL && ! args.isInitialSuggestions ? results[ 0 ].concat( results[ 1 ] ) : results[ 0 ];
};

// Effects
const getSearchHandler = useCallback( ( val, args ) => {
const protocol = getProtocol( val ) || '';
const isMailto = protocol.includes( 'mailto' );
const isInternal = startsWith( val, '#' );
const isTel = protocol.includes( 'tel' );

const handleManualEntry = isInternal || isMailto || isTel || isURL( val ) || ( val && val.includes( 'www.' ) );

return ( handleManualEntry ) ? handleDirectEntry( val, args ) : handleEntitySearch( val, args );
}, [ handleDirectEntry, fetchSearchSuggestions ] );
async function getSearchSuggestions( search, { isInitialSuggestions } ) {
return ( await Promise.all( [
fetchSearchSuggestions( search, isInitialSuggestions ? { perPage: 3 } : {} ),
Promise.resolve( [ getSuggestionByURL( search ) ] ),
] ) ).flat();
}

// Render Components
const renderSearchResults = ( { suggestionsListProps, buildSuggestionItemProps, suggestions, selectedSuggestion, isLoading, isInitialSuggestions } ) => {
const resultsListClasses = classnames( 'block-editor-link-control__search-results', {
'is-loading': isLoading,
} );

const manualLinkEntryTypes = [ 'url', 'mailto', 'tel', 'internal' ];
const searchResultsLabelId = isInitialSuggestions ? `block-editor-link-control-search-results-label-${ instanceId }` : undefined;
const labelText = isInitialSuggestions ? __( 'Recently updated' ) : sprintf( __( 'Search results for %s' ), inputValue );
// According to guidelines aria-label should be added if the label
Expand Down Expand Up @@ -146,7 +92,6 @@ function LinkControl( {
onChange( { ...value, ...suggestion } );
} }
isSelected={ index === selectedSuggestion }
isURL={ manualLinkEntryTypes.includes( suggestion.type.toLowerCase() ) }
searchTerm={ inputValue }
/>
) ) }
Expand Down Expand Up @@ -203,7 +148,7 @@ function LinkControl( {
onChange( { ...value, ...suggestion } );
} }
renderSuggestions={ renderSearchResults }
fetchSuggestions={ getSearchHandler }
fetchSuggestions={ getSearchSuggestions }
onReset={ resetInput }
showInitialSuggestions={ showInitialSuggestions }
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { LEFT,
/**
* Internal dependencies
*/
import getSuggestionByURL from './get-suggestion-by-url';
import { URLInput } from '../';

const handleLinkControlOnKeyDown = ( event ) => {
Expand Down Expand Up @@ -58,7 +59,7 @@ const LinkControlSearchInput = ( {

// Interpret the selected value as either the selected suggestion, if
// exists, or otherwise the current input value as entered.
onSelect( selectedSuggestion || { url: value } );
onSelect( selectedSuggestion || getSuggestionByURL( value ) );
}

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import {
TextHighlight,
} from '@wordpress/components';

export const LinkControlSearchItem = ( { itemProps, suggestion, isSelected = false, onClick, isURL = false, searchTerm = '' } ) => {
export const LinkControlSearchItem = ( { itemProps, suggestion, isSelected = false, onClick, searchTerm = '' } ) => {
const isURL = suggestion.type === 'url';

return (
<Button
{ ...itemProps }
Expand All @@ -30,7 +32,7 @@ export const LinkControlSearchItem = ( { itemProps, suggestion, isSelected = fal
) }
<span className="block-editor-link-control__search-item-header">
<span className="block-editor-link-control__search-item-title">
<TextHighlight text={ suggestion.title } highlight={ searchTerm } />
<TextHighlight text={ suggestion.title || suggestion.url } highlight={ searchTerm } />
</span>
<span aria-hidden={ ! isURL } className="block-editor-link-control__search-item-info">
{ ! isURL && ( safeDecodeURI( suggestion.url ) || '' ) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,29 @@ export const fauxEntitySuggestions = [
{
id: uniqueId(),
title: 'Hello Page',
type: 'Page',
info: '2 days ago',
type: 'post',
subtype: 'page',
url: `?p=${ uniqueId() }`,
},
{
id: uniqueId(),
title: 'Hello Post',
type: 'Post',
info: '19 days ago',
type: 'post',
subtype: 'post',
url: `?p=${ uniqueId() }`,
},
{
id: uniqueId(),
title: 'Hello Another One',
type: 'Page',
info: '19 days ago',
type: 'post',
subtype: 'page',
url: `?p=${ uniqueId() }`,
},
{
id: uniqueId(),
title: 'This is another Post with a much longer title just to be really annoying and to try and break the UI',
type: 'Post',
type: 'post',
subtype: 'post',
info: '1 month ago',
url: `?p=${ uniqueId() }`,
},
Expand Down
14 changes: 8 additions & 6 deletions packages/block-editor/src/components/link-control/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -453,10 +453,11 @@ describe( 'Selecting links', () => {
it.each( [
[ 'entity', 'hello world', first( fauxEntitySuggestions ) ], // entity search
[ 'url', 'https://www.wordpress.org', {
id: '1',
title: 'https://www.wordpress.org',
id: 'https://www.wordpress.org',
title: '',
url: 'https://www.wordpress.org',
type: 'URL',
type: 'url',
subtype: 'https',
} ], // url
] )( 'should display a current selected link UI when a %s suggestion for the search "%s" is clicked', async ( type, searchTerm, selectedLink ) => {
const LinkControlConsumer = () => {
Expand Down Expand Up @@ -512,10 +513,11 @@ describe( 'Selecting links', () => {
it.each( [
[ 'entity', 'hello world', first( fauxEntitySuggestions ) ], // entity search
[ 'url', 'https://www.wordpress.org', {
id: '1',
title: 'https://www.wordpress.org',
id: 'https://www.wordpress.org',
title: '',
url: 'https://www.wordpress.org',
type: 'URL',
type: 'url',
subtype: 'https',
} ], // url
] )( 'should display a current selected link UI when an %s suggestion for the search "%s" is selected using the keyboard', async ( type, searchTerm, selectedLink ) => {
const LinkControlConsumer = () => {
Expand Down