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

Experimental Link creation interface #17846

Merged
merged 113 commits into from
Oct 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
113 commits
Select commit Hold shift + click to select a range
8dbc2bd
Initial component file structure
getdave Oct 8, 2019
ee008be
Implement basic icon and toggle mechanic
getdave Oct 8, 2019
e685003
Adds basic search input
getdave Oct 8, 2019
70049ad
Update input to utilise LinkEditor component autocomplete
getdave Oct 8, 2019
98e6aa5
Add ability to customise placeholder
getdave Oct 8, 2019
85a6317
Update to utilise URLInput directly for greater flexibility
getdave Oct 8, 2019
8ec1488
Add example search results and test coverage
getdave Oct 8, 2019
140cc90
Update class naming convention to match guidelines
getdave Oct 9, 2019
d64608e
Adds render prop to enable custom suggestions rendering
getdave Oct 9, 2019
30e0eb6
Update to utilise URLInput render prop to customise search suggestion…
getdave Oct 9, 2019
53ce517
Update to add post type to the fetchLinkSuggestions responsive mapping
getdave Oct 9, 2019
b53c13c
Fix to ensure search suggestion interaction states are perceivable
getdave Oct 9, 2019
6d7259a
Update suggestion render prop to provide component props as arguments
getdave Oct 9, 2019
4f32429
Update to match with design visual and provide more accessible markup
getdave Oct 9, 2019
4971e30
Adds settings area. Fixes missing reset icon.
getdave Oct 9, 2019
4aea19c
Fix search items to be buttons with correct style and layout
getdave Oct 10, 2019
d554b65
Adds overflow scrolling to search results
getdave Oct 10, 2019
26c4c1b
Fix to stop scroll shadow overlaying scrollbars
getdave Oct 10, 2019
e778a04
Add bespoke settings area and tweak styles
getdave Oct 10, 2019
3ca482e
Update to allow URLs to be conditionally handled as a suggestion
getdave Oct 10, 2019
aec9f39
Updates to conditionally use an entity or url based search results fe…
getdave Oct 10, 2019
70bd877
Fix bug whereby fetchSearchSuggestions wasn’t called
getdave Oct 11, 2019
58220f8
Remove default toggle UI and implement Popover close
getdave Oct 11, 2019
5eeb819
Adds search text “highlighting” in results list
getdave Oct 11, 2019
d355133
Move TextHighlight component to its own file
getdave Oct 11, 2019
2044065
Fix bug where update to value prop didn’t cause suggestions to reset.
getdave Oct 11, 2019
b256d55
Update to remove internal handling of open/closed state
getdave Oct 11, 2019
b55f557
Fix React violation by returning only the text for non matches
getdave Oct 11, 2019
2133fb1
Update existing tests to match new implementation
getdave Oct 11, 2019
70c5c04
Add link reset test
getdave Oct 11, 2019
4a031ed
Adds test which uncovers major bug in the implementation
getdave Oct 11, 2019
5b5164c
Tweak critical test to be more explicit about what is expected
getdave Oct 11, 2019
664daad
Fix bug to make determining search handler use the latest input value
getdave Oct 14, 2019
9780959
Add loading spinner and associated test coverage
getdave Oct 14, 2019
5c3bf35
Fix bug where value could be empty
getdave Oct 14, 2019
c962dc6
Adds basic editing / view state switching
getdave Oct 15, 2019
d4c0a70
Add keydown callback to URLInput
getdave Oct 15, 2019
8cff4c2
Select link on ENTER keydown event
getdave Oct 15, 2019
b4a3f66
Utilise LinkViewer to render edit state and decode urls for display
getdave Oct 15, 2019
fbe49e7
Only display link settings when a link is selected
getdave Oct 15, 2019
fa0dd36
Adds current link view styles
getdave Oct 15, 2019
f5a7e65
Makes settings toggle controlled by parent component
getdave Oct 15, 2019
a88826e
Update visuals to match updated design
getdave Oct 16, 2019
ec9e2ad
Add standardised min width to popover
getdave Oct 16, 2019
11ca79f
Temporary hack to include Link UI in Playground for testing
getdave Oct 16, 2019
779d440
Update to utilise isURL util from @wordpress/url package
getdave Oct 16, 2019
4941bb8
Update to utilise isURL util from @wordpress/url package
getdave Oct 16, 2019
452ec55
Removes URLPopover dependency
getdave Oct 16, 2019
5484a7c
Extract settings drawer to sub component
getdave Oct 16, 2019
37de5d9
Refactor search items into a component
getdave Oct 17, 2019
ba32148
Refactor Input and Search to component
getdave Oct 17, 2019
33467f3
Fix missing selected state on search suggestions
getdave Oct 17, 2019
553be99
Tweak line height on search suggestion url path
getdave Oct 17, 2019
ad46c87
Augment test for URL-like by testing for “www.”
getdave Oct 18, 2019
26c292f
Fix to stop url overflows and wrapping on to multiple lines
getdave Oct 18, 2019
17be2f8
Uppcase URL in type indicator within search results list
getdave Oct 21, 2019
13f5ce1
Avoid reading out slug/URL for entity results
getdave Oct 21, 2019
556088a
Ensures i18n of change button
getdave Oct 21, 2019
1cdd939
Always offer URL result in search suggestions as default
getdave Oct 21, 2019
2588b9e
Fix loading spinner position and dim results during loading
getdave Oct 21, 2019
a065936
Fix scroll shadows to use valid alpha transparent values in gradient
getdave Oct 21, 2019
98836fd
Adds instructional text in place of URL for suggestions that are URLs
getdave Oct 23, 2019
5254015
Update prop names for consistency
getdave Oct 23, 2019
811ad11
Update line length to improve readability
getdave Oct 23, 2019
9cd6d18
Update to avoid need to utilise partialRight util from lodash
getdave Oct 23, 2019
bdb6217
Updates key to avoid usage of index
getdave Oct 23, 2019
d0a348b
Update to remote isFunction check in favour of direct check
getdave Oct 23, 2019
da212f0
Update to handle mailto and tel protocols and internal links
getdave Oct 23, 2019
19d5e64
url-input: handle onKeyPress type event
retrofox Oct 22, 2019
548279b
link-control: add className prop
retrofox Oct 22, 2019
a844800
link-control: add README file
retrofox Oct 22, 2019
fd3a6ef
Remove unnecessary use of useCallback
getdave Oct 24, 2019
b314c49
Fix current automated tests
getdave Oct 24, 2019
5aeb531
Improves URL handling test to run for multiple URL value variations
getdave Oct 24, 2019
1a7c285
Updates to display the URL type in the search results
getdave Oct 24, 2019
63201c9
Refactor tests to assert against all valid protocol formats and link …
getdave Oct 24, 2019
4918184
Adds test to cover display of fallback URL search result for search v…
getdave Oct 24, 2019
cd29ab5
Adds tests to check URL suggestions don’t display for non-URLs.
getdave Oct 24, 2019
2d8befb
url-input: remove unneeded `suggestion` const
retrofox Oct 23, 2019
d8895d5
url-input: always trigger onKeyDown event
retrofox Oct 23, 2019
2413016
link-control: delegate handling keydown event
retrofox Oct 23, 2019
e3042c8
link-control: add onKeyDown and onKeyPress handlers
retrofox Oct 24, 2019
0a9d558
link-control: playground -> close once onClose
retrofox Oct 24, 2019
89e92fb
link-control: propagate onClose() event
retrofox Oct 24, 2019
ff57160
link-control: playground -> hanldling close by ESCAPE key
retrofox Oct 24, 2019
48e5f44
Fix to only render settings draw if settings are defined
getdave Oct 24, 2019
4e811db
Remove redundant commented out test
getdave Oct 24, 2019
43c30b2
Update to render with a “current link” if one is provided.
getdave Oct 24, 2019
158ea3e
Render playground with currentLink active
getdave Oct 24, 2019
4981a71
Adds test to cover currentLink prop
getdave Oct 25, 2019
f1c54a6
Remove selected state from Playground
getdave Oct 25, 2019
797fd6c
Adds tests to cover selecting and changing links
getdave Oct 25, 2019
e5e44e6
Remove async function in place of direct Promise usage and add test c…
getdave Oct 25, 2019
5ba4b65
Add test to cover keyboard handling
getdave Oct 25, 2019
d5abad2
Remove unecessary dep from effect
getdave Oct 25, 2019
5c1ec22
Fix URLInput to pass the actual suggestion object not the index
getdave Oct 25, 2019
4d5b455
Fix keyboard handling so hitting `ENTER` will select an item as the c…
getdave Oct 25, 2019
b8a01bd
Updates keyboard interaction test to include URL entry
getdave Oct 25, 2019
7df9aa5
Minor: reword test description
getdave Oct 25, 2019
b6edb73
Fix missing key prop regression
getdave Oct 25, 2019
b435b49
DRY up conditionals
getdave Oct 25, 2019
8c5ed79
link-control: set a default experimental link suggestions searcher if…
retrofox Oct 24, 2019
1fffe8e
link-control: handling key events
retrofox Oct 25, 2019
1c1614d
url-input: remove onKeyDown prop
retrofox Oct 25, 2019
0cdf706
url-input: remove calling onKeyDown prop
retrofox Oct 28, 2019
2f80347
url-input: rollback some changes
retrofox Oct 24, 2019
b93a0db
Mark Link Creation Interface as Experimental (#18110)
marekhrabe Oct 28, 2019
5b6715d
Remove unused prop from docs
getdave Oct 28, 2019
280db32
Update props ordering and readme docs
obenland Oct 29, 2019
ba985eb
Revert playground changes
obenland Oct 29, 2019
7d35791
Rename InputSearch to SearchInput
obenland Oct 29, 2019
e7585b8
Remove disabling of jsx-key lint rule
talldan Oct 30, 2019
8450bb1
Change fake id value to something that will not clash with post ids
talldan Oct 30, 2019
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
1 change: 1 addition & 0 deletions packages/block-editor/src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export { default as __experimentalGradientPickerControl } from './gradient-picke
export { default as InnerBlocks } from './inner-blocks';
export { default as InspectorAdvancedControls } from './inspector-advanced-controls';
export { default as InspectorControls } from './inspector-controls';
export { default as __experimentalLinkControl } from './link-control';
export { default as MediaPlaceholder } from './media-placeholder';
export { default as MediaUpload } from './media-upload';
export { default as MediaUploadCheck } from './media-upload/check';
Expand Down
50 changes: 50 additions & 0 deletions packages/block-editor/src/components/link-control/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Link Control

## Props

### className

- Type: `String`
- Required: Yes

### currentLink

- Type: `Object`
- Required: Yes

### currentSettings

- Type: `Object`
- Required: Yes

### fetchSearchSuggestions

- Type: `Function`
- Required: No

## Event handlers

### onClose

- Type: `Function`
- Required: No

### onKeyDown

- Type: `Function`
- Required: No

### onKeyPress

- Type: `Function`
- Required: No

### onLinkChange

- Type: `Function`
- Required: No

### onSettingChange

- Type: `Function`
- Required: No
249 changes: 249 additions & 0 deletions packages/block-editor/src/components/link-control/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
/**
* External dependencies
*/
import classnames from 'classnames';
import { isFunction, noop, startsWith } from 'lodash';

/**
* WordPress dependencies
*/
import {
Button,
ExternalLink,
Popover,
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';

import {
useCallback,
useState,
useEffect,
Fragment,
} from '@wordpress/element';

import {
safeDecodeURI,
filterURLForDisplay,
isURL,
prependHTTP,
getProtocol,
} from '@wordpress/url';

import { withInstanceId, compose } from '@wordpress/compose';
import { withSelect } from '@wordpress/data';

/**
* Internal dependencies
*/
import LinkControlSettingsDrawer from './settings-drawer';
import LinkControlSearchItem from './search-item';
import LinkControlSearchInput from './search-input';

function LinkControl( {
className,
currentLink,
currentSettings,
fetchSearchSuggestions,
instanceId,
onClose = noop,
onKeyDown = noop,
onKeyPress = noop,
onLinkChange = noop,
onSettingsChange = { noop },
} ) {
// State
const [ inputValue, setInputValue ] = useState( '' );
const [ isEditingLink, setIsEditingLink ] = useState( false );

// Effects
useEffect( () => {
// If we have a link then stop editing mode
if ( currentLink ) {
setIsEditingLink( false );
} else {
setIsEditingLink( true );
}
}, [ currentLink ] );

// Handlers

/**
* onChange LinkControlSearchInput event handler
*
* @param {string} value Current value returned by the search.
*/
const onInputChange = ( value = '' ) => {
setInputValue( value );
};

// Utils
const startEditMode = () => {
if ( isFunction( onLinkChange ) ) {
onLinkChange();
}
};

const closeLinkUI = () => {
resetInput();
onClose();
};

const resetInput = () => {
setInputValue( '' );
};

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

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

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

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

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

return Promise.resolve(
[ {
id: '-1',
title: value,
url: type === 'URL' ? prependHTTP( value ) : value,
type,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was there any planned usage for the various types associated with manual entry? As part of the ongoing discussion in #18061, I've been considering whether this is something where the explicit absence of the type could be a reasonably good indicator on its own for marking a value as "manual entry". The sorts of things that someone might want to do with type should be equally achievable with the @wordpress/url utilities, if a developer were so inclined. One of the issues I encountered in #19827 was trying to decide what the best complete set of these "subtypes" should be, where we would probably be better off to just not have to answer that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was there any planned usage for the various types associated with manual entry? As part of the ongoing discussion in #18061, I've been considering whether this is something where the explicit absence of the type could be a reasonably good indicator on its own for marking a value as "manual entry". The sorts of things that someone might want to do with type should be equally achievable with the @wordpress/url utilities, if a developer were so inclined. One of the issues I encountered in #19827 was trying to decide what the best complete set of these "subtypes" should be, where we would probably be better off to just not have to answer that.

Follow-up: #20051

} ]
);
};

const handleEntitySearch = async ( value ) => {
const results = await Promise.all( [
fetchSearchSuggestions( value ),
handleDirectEntry( value ),
] );

const couldBeURL = ! value.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 ? results[ 0 ].concat( results[ 1 ] ) : results[ 0 ];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be tidier if all the code from handleURLSearch and handleEntitySearch were defined in getSearchHandler. There seems to be a lot of logic around isURL, couldBeURL, value.includes( 'www.' ) that could be streamlined by moving all the code to be in the same place.

Other than that, this could use an array spread:

return couldBeURL ? [ ...results[ 0 ], ...results[ 1 ] ]  : results[ 0 ];

};

// Effects
const getSearchHandler = useCallback( ( value ) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getSearchHandler is an 'effect' according to the comment, but resetInput isn't. Both seem to use useCallback. The name could possibly be changed as it doesn't return a searchHandler from what I can tell, but the searchResults instead.

const protocol = getProtocol( value ) || '';
const isMailto = protocol.includes( 'mailto' );
const isInternal = startsWith( value, '#' );
const isTel = protocol.includes( 'tel' );

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

return ( handleManualEntry ) ? handleDirectEntry( value ) : handleEntitySearch( value );
}, [ handleDirectEntry, fetchSearchSuggestions ] );

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

const manualLinkEntryTypes = [ 'url', 'mailto', 'tel', 'internal' ];

return (
<div className="block-editor-link-control__search-results-wrapper">
<div { ...suggestionsListProps } className={ resultsListClasses }>
{ suggestions.map( ( suggestion, index ) => (
<LinkControlSearchItem
key={ `${ suggestion.id }-${ suggestion.type }` }
itemProps={ buildSuggestionItemProps( suggestion, index ) }
suggestion={ suggestion }
onClick={ () => onLinkChange( suggestion ) }
isSelected={ index === selectedSuggestion }
isURL={ manualLinkEntryTypes.includes( suggestion.type.toLowerCase() ) }
searchTerm={ inputValue }
/>
) ) }
</div>
</div>
);
};

return (
<Popover
className={ classnames( 'block-editor-link-control', className ) }
onClose={ closeLinkUI }
talldan marked this conversation as resolved.
Show resolved Hide resolved
position="bottom center"
focusOnMount="firstElement"
>
<div className="block-editor-link-control__popover-inner">
<div className="block-editor-link-control__search">

{ ( ! isEditingLink && currentLink ) && (
<Fragment>
<p className="screen-reader-text" id={ `current-link-label-${ instanceId }` }>
{ __( 'Currently selected' ) }:
</p>
<div
aria-labelledby={ `current-link-label-${ instanceId }` }
aria-selected="true"
className={ classnames( 'block-editor-link-control__search-item', {
'is-current': true,
} ) }
>
<span className="block-editor-link-control__search-item-header">

<ExternalLink
className="block-editor-link-control__search-item-title"
href={ currentLink.url }
>
{ currentLink.title }
</ExternalLink>
<span className="block-editor-link-control__search-item-info">{ filterURLForDisplay( safeDecodeURI( currentLink.url ) ) || '' }</span>
</span>

<Button isDefault onClick={ startEditMode } className="block-editor-link-control__search-item-action block-editor-link-control__search-item-action--edit">
aduth marked this conversation as resolved.
Show resolved Hide resolved
{ __( 'Change' ) }
</Button>
</div>
</Fragment>
) }

{ isEditingLink && (
<LinkControlSearchInput
value={ inputValue }
onChange={ onInputChange }
onSelect={ onLinkChange }
renderSuggestions={ renderSearchResults }
fetchSuggestions={ getSearchHandler }
onReset={ resetInput }
onKeyDown={ onKeyDown }
onKeyPress={ onKeyPress }
/>
) }

{ ! isEditingLink && (
<LinkControlSettingsDrawer settings={ currentSettings } onSettingChange={ onSettingsChange } />
) }
</div>
</div>
</Popover>
);
}

export default compose(
withInstanceId,
withSelect( ( select, ownProps ) => {
if ( ownProps.fetchSearchSuggestions && isFunction( ownProps.fetchSearchSuggestions ) ) {
return;
}

const { getSettings } = select( 'core/block-editor' );
return {
fetchSearchSuggestions: getSettings().__experimentalFetchLinkSuggestions,
};
} )
)( LinkControl );
69 changes: 69 additions & 0 deletions packages/block-editor/src/components/link-control/search-input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@

/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { IconButton } from '@wordpress/components';
import { ENTER } from '@wordpress/keycodes';

/**
* Internal dependencies
*/
import { URLInput } from '../';

const LinkControlSearchInput = ( {
value,
onChange,
onSelect,
renderSuggestions,
fetchSuggestions,
onReset,
onKeyDown,
onKeyPress,
} ) => {
const selectItemHandler = ( selection, suggestion ) => {
onChange( selection );

if ( suggestion ) {
onSelect( suggestion );
}
};

const stopFormEventsPropagation = ( event ) => {
event.preventDefault();
event.stopPropagation();
};

return (
<form onSubmit={ stopFormEventsPropagation }>
<URLInput
className="block-editor-link-control__search-input"
value={ value }
onChange={ selectItemHandler }
onKeyDown={ ( event ) => {
if ( event.keyCode === ENTER ) {
return;
}
onKeyDown( event );
} }
onKeyPress={ onKeyPress }
placeholder={ __( 'Search or type url' ) }
__experimentalRenderSuggestions={ renderSuggestions }
__experimentalFetchLinkSuggestions={ fetchSuggestions }
__experimentalHandleURLSuggestions={ true }
/>

<IconButton
disabled={ ! value.length }
type="reset"
label={ __( 'Reset' ) }
icon="no-alt"
className="block-editor-link-control__search-reset"
onClick={ onReset }
/>

</form>
);
};

export default LinkControlSearchInput;
Loading